claude-memory-agent 2.1.0 → 2.2.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.
Files changed (91) hide show
  1. package/bin/cli.js +11 -1
  2. package/bin/lib/banner.js +39 -0
  3. package/bin/lib/environment.js +166 -0
  4. package/bin/lib/installer.js +291 -0
  5. package/bin/lib/models.js +95 -0
  6. package/bin/lib/steps/advanced.js +101 -0
  7. package/bin/lib/steps/confirm.js +87 -0
  8. package/bin/lib/steps/model.js +57 -0
  9. package/bin/lib/steps/provider.js +65 -0
  10. package/bin/lib/steps/scope.js +59 -0
  11. package/bin/lib/steps/server.js +74 -0
  12. package/bin/lib/ui.js +75 -0
  13. package/bin/onboarding.js +164 -0
  14. package/bin/postinstall.js +22 -257
  15. package/config.py +103 -4
  16. package/dashboard.html +697 -27
  17. package/hooks/extract_memories.py +439 -0
  18. package/hooks/pre_compact_hook.py +76 -0
  19. package/hooks/session_end_hook.py +149 -0
  20. package/hooks/stop_hook.py +372 -0
  21. package/install.py +85 -32
  22. package/main.py +1636 -892
  23. package/mcp_server.py +451 -0
  24. package/package.json +14 -3
  25. package/requirements.txt +12 -8
  26. package/services/adaptive_ranker.py +272 -0
  27. package/services/agent_catalog.json +153 -0
  28. package/services/agent_registry.py +245 -730
  29. package/services/claude_md_sync.py +320 -4
  30. package/services/consolidation.py +417 -0
  31. package/services/database.py +586 -105
  32. package/services/embedding_pipeline.py +262 -0
  33. package/services/embeddings.py +493 -85
  34. package/services/memory_decay.py +408 -0
  35. package/services/native_memory_paths.py +86 -0
  36. package/services/native_memory_sync.py +496 -0
  37. package/services/response_manager.py +183 -0
  38. package/services/terminal_ui.py +199 -0
  39. package/services/tier_manager.py +235 -0
  40. package/services/websocket.py +26 -6
  41. package/skills/search.py +136 -61
  42. package/skills/session_review.py +210 -23
  43. package/skills/store.py +125 -18
  44. package/terminal_dashboard.py +474 -0
  45. package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
  46. package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
  47. package/hooks/__pycache__/grounding-hook.cpython-312.pyc +0 -0
  48. package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
  49. package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  50. package/services/__pycache__/__init__.cpython-312.pyc +0 -0
  51. package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
  52. package/services/__pycache__/auth.cpython-312.pyc +0 -0
  53. package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
  54. package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
  55. package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
  56. package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
  57. package/services/__pycache__/confidence.cpython-312.pyc +0 -0
  58. package/services/__pycache__/curator.cpython-312.pyc +0 -0
  59. package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
  60. package/services/__pycache__/database.cpython-312.pyc +0 -0
  61. package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
  62. package/services/__pycache__/insights.cpython-312.pyc +0 -0
  63. package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
  64. package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
  65. package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
  66. package/services/__pycache__/timeline.cpython-312.pyc +0 -0
  67. package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
  68. package/services/__pycache__/websocket.cpython-312.pyc +0 -0
  69. package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
  70. package/skills/__pycache__/admin.cpython-312.pyc +0 -0
  71. package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
  72. package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
  73. package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
  74. package/skills/__pycache__/confidence_tracker.cpython-312.pyc +0 -0
  75. package/skills/__pycache__/context.cpython-312.pyc +0 -0
  76. package/skills/__pycache__/curator.cpython-312.pyc +0 -0
  77. package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
  78. package/skills/__pycache__/insights.cpython-312.pyc +0 -0
  79. package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
  80. package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
  81. package/skills/__pycache__/search.cpython-312.pyc +0 -0
  82. package/skills/__pycache__/session_review.cpython-312.pyc +0 -0
  83. package/skills/__pycache__/state.cpython-312.pyc +0 -0
  84. package/skills/__pycache__/store.cpython-312.pyc +0 -0
  85. package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
  86. package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
  87. package/skills/__pycache__/verification.cpython-312.pyc +0 -0
  88. package/test_automation.py +0 -221
  89. package/test_complete.py +0 -338
  90. package/test_full.py +0 -322
  91. package/verify_db.py +0 -134
package/main.py CHANGED
@@ -39,6 +39,7 @@ from services.database import (
39
39
  )
40
40
  from services.embeddings import EmbeddingService
41
41
  from services.auth import get_auth_service, AuthService
42
+ from services.response_manager import fit_response
42
43
 
43
44
  # Original memory skills
44
45
  from skills.store import store_memory, store_project, store_pattern
@@ -110,14 +111,83 @@ from services.claude_md_sync import get_claude_md_sync
110
111
  # Agent registry for dashboard
111
112
  from services.agent_registry import (
112
113
  AVAILABLE_AGENTS, AVAILABLE_MCPS, AVAILABLE_HOOKS,
113
- AGENT_CATEGORIES, get_agents_by_category, get_agent_by_id
114
+ AGENT_CATEGORIES, get_agents_by_category, get_agent_by_id,
115
+ load_configured_hooks, load_configured_mcps
114
116
  )
115
117
 
116
118
  load_dotenv()
117
119
 
120
+ # ---------------------------------------------------------------------------
121
+ # Simple metrics tracker for search/store operations
122
+ # ---------------------------------------------------------------------------
123
+ class OperationMetrics:
124
+ """Lightweight in-memory metrics for search and store operations.
125
+
126
+ Tracks hit rates, result counts, and operation frequencies to answer:
127
+ "Are stored memories actually being found?"
128
+ """
129
+
130
+ def __init__(self):
131
+ self.search_total = 0
132
+ self.search_hits = 0 # searches that returned >= 1 result
133
+ self.search_empty = 0 # searches that returned 0 results
134
+ self.search_result_counts: List[int] = [] # rolling window of result counts
135
+ self.store_total = 0
136
+ self.store_merged = 0 # dedup merges
137
+ self.store_without_embedding = 0
138
+ self._max_history = 1000 # keep last 1000 result counts
139
+
140
+ def record_search(self, result_count: int):
141
+ self.search_total += 1
142
+ if result_count > 0:
143
+ self.search_hits += 1
144
+ else:
145
+ self.search_empty += 1
146
+ self.search_result_counts.append(result_count)
147
+ if len(self.search_result_counts) > self._max_history:
148
+ self.search_result_counts = self.search_result_counts[-self._max_history:]
149
+
150
+ def record_store(self, merged: bool = False, has_embedding: bool = True):
151
+ self.store_total += 1
152
+ if merged:
153
+ self.store_merged += 1
154
+ if not has_embedding:
155
+ self.store_without_embedding += 1
156
+
157
+ def to_dict(self) -> dict:
158
+ avg_results = (
159
+ sum(self.search_result_counts) / len(self.search_result_counts)
160
+ if self.search_result_counts else 0
161
+ )
162
+ hit_rate = (
163
+ self.search_hits / self.search_total
164
+ if self.search_total > 0 else 0
165
+ )
166
+ return {
167
+ "search": {
168
+ "total": self.search_total,
169
+ "hits": self.search_hits,
170
+ "empty": self.search_empty,
171
+ "hit_rate": round(hit_rate, 3),
172
+ "avg_result_count": round(avg_results, 1)
173
+ },
174
+ "store": {
175
+ "total": self.store_total,
176
+ "merged": self.store_merged,
177
+ "without_embedding": self.store_without_embedding
178
+ }
179
+ }
180
+
181
+
182
+ metrics = OperationMetrics()
183
+
118
184
  # Initialize services
119
185
  db = DatabaseService()
120
- embeddings = EmbeddingService()
186
+ from config import config as _cfg
187
+ embeddings = EmbeddingService(
188
+ provider_type=_cfg.EMBEDDING_PROVIDER,
189
+ model=_cfg.EMBEDDING_MODEL,
190
+ )
121
191
 
122
192
  # Retry queue (imported lazily to avoid circular imports)
123
193
  retry_queue = None
@@ -182,19 +252,84 @@ async def lifespan(app: FastAPI):
182
252
  run_curator_scheduler(db, embeddings, interval_hours=curator_interval)
183
253
  )
184
254
 
185
- print(f"Memory Agent v2.0 started on port {os.getenv('PORT', 8102)}")
186
- print(f"Retry queue initialized (depth: {retry_queue.get_queue_depth()})")
187
- print(f"Curator scheduler started (interval: {curator_interval}h)")
255
+ # Initialize embedding pipeline with LRU cache
256
+ from services.embedding_pipeline import get_embedding_pipeline
257
+ pipeline = get_embedding_pipeline(embeddings, db)
258
+
259
+ # Start embedding pre-computation background loop
260
+ from config import config as app_config
188
261
 
189
- # Show auth status
262
+ async def precompute_loop():
263
+ """Background: generate embeddings for memories missing them."""
264
+ interval = app_config.EMBEDDING_PRECOMPUTE_INTERVAL
265
+ while True:
266
+ await asyncio.sleep(interval)
267
+ try:
268
+ result = await pipeline.precompute_missing_embeddings()
269
+ if result.get('generated', 0) > 0:
270
+ logger.info(f"Pre-computed {result['generated']} embeddings")
271
+ except Exception as e:
272
+ logger.debug(f"Precompute loop error: {e}")
273
+
274
+ precompute_task = asyncio.create_task(precompute_loop())
275
+
276
+ # Start consolidation background loop
277
+ async def consolidation_loop():
278
+ """Background: consolidate similar warm-tier memories."""
279
+ interval_hours = app_config.CONSOLIDATION_INTERVAL_HOURS
280
+ while True:
281
+ await asyncio.sleep(interval_hours * 3600)
282
+ try:
283
+ from services.consolidation import ConsolidationService
284
+ consolidator = ConsolidationService(db, embeddings)
285
+ result = await consolidator.run_consolidation()
286
+ if result.get('consolidated', 0) > 0:
287
+ logger.info(
288
+ f"Consolidated {result['consolidated']} groups "
289
+ f"({result['memories_archived']} memories archived)"
290
+ )
291
+ except Exception as e:
292
+ logger.debug(f"Consolidation loop error: {e}")
293
+
294
+ consolidation_task = asyncio.create_task(consolidation_loop())
295
+
296
+ # Collect DB stats for splash
190
297
  auth_stats = auth_service.get_stats()
191
- if auth_stats["enabled"]:
192
- print(f"Authentication: ENABLED ({auth_stats['active_keys']} active keys)")
193
- print(f" Key file: {auth_stats['key_file']}")
194
- if auth_stats['active_keys'] == 1:
195
- print(" Note: Default key generated. Check key file for the key hash.")
196
- else:
197
- print("Authentication: DISABLED (set AUTH_ENABLED=true to enable)")
298
+ db_stats = None
299
+ try:
300
+ db_stats = await db.get_stats()
301
+ except Exception:
302
+ pass
303
+
304
+ # Rich terminal splash screen
305
+ try:
306
+ from services.terminal_ui import print_splash, setup_rich_logging
307
+
308
+ print_splash(
309
+ version="2.4.0",
310
+ port=int(os.getenv("PORT", 8102)),
311
+ auth_enabled=auth_stats.get("enabled", False),
312
+ auth_keys=auth_stats.get("active_keys", 0),
313
+ queue_depth=retry_queue.get_queue_depth(),
314
+ curator_interval=curator_interval,
315
+ embedding_cache_size=app_config.EMBEDDING_CACHE_SIZE,
316
+ precompute_interval=app_config.EMBEDDING_PRECOMPUTE_INTERVAL,
317
+ consolidation_threshold=app_config.CONSOLIDATION_THRESHOLD,
318
+ consolidation_interval=app_config.CONSOLIDATION_INTERVAL_HOURS,
319
+ db_stats=db_stats,
320
+ )
321
+
322
+ # Install rich logging handler for prettier output
323
+ rich_handler = setup_rich_logging(app_config.LOG_LEVEL)
324
+ logging.root.handlers = [rich_handler]
325
+
326
+ except ImportError:
327
+ # Fallback to plain output if rich unavailable
328
+ print(f"Memory Agent v2.4.0 (CLaRa) started on port {os.getenv('PORT', 8102)}")
329
+ if auth_stats.get("enabled"):
330
+ print(f"Authentication: ENABLED ({auth_stats.get('active_keys', 0)} active keys)")
331
+ else:
332
+ print("Authentication: DISABLED")
198
333
 
199
334
  yield
200
335
 
@@ -202,14 +337,13 @@ async def lifespan(app: FastAPI):
202
337
  retry_queue.stop_processing()
203
338
  queue_task.cancel()
204
339
  curator_task.cancel()
205
- try:
206
- await queue_task
207
- except asyncio.CancelledError:
208
- pass
209
- try:
210
- await curator_task
211
- except asyncio.CancelledError:
212
- pass
340
+ precompute_task.cancel()
341
+ consolidation_task.cancel()
342
+ for task in [queue_task, curator_task, precompute_task, consolidation_task]:
343
+ try:
344
+ await task
345
+ except asyncio.CancelledError:
346
+ pass
213
347
  retry_queue.close()
214
348
  await db.disconnect()
215
349
 
@@ -217,7 +351,7 @@ async def lifespan(app: FastAPI):
217
351
  app = FastAPI(
218
352
  title="Claude Memory Agent",
219
353
  description="Persistent semantic memory for Claude Code sessions with cross-project support",
220
- version="2.0.0",
354
+ version="2.4.0",
221
355
  lifespan=lifespan
222
356
  )
223
357
 
@@ -358,7 +492,7 @@ async def handle_task_send(request: A2ARequest) -> JSONResponse:
358
492
  "result": {
359
493
  "id": task_id,
360
494
  "status": {"state": "completed"},
361
- "artifacts": [{"parts": [{"type": "text", "text": json.dumps(result, indent=2)}]}]
495
+ "artifacts": [{"parts": [{"type": "text", "text": fit_response(result)}]}]
362
496
  }
363
497
  })
364
498
 
@@ -394,7 +528,7 @@ async def handle_task_get(request: A2ARequest) -> JSONResponse:
394
528
  "result": {
395
529
  "id": task_id,
396
530
  "status": {"state": task["status"]},
397
- "artifacts": [{"parts": [{"type": "text", "text": json.dumps(task.get("result", {}), indent=2)}]}] if task.get("result") else []
531
+ "artifacts": [{"parts": [{"type": "text", "text": fit_response(task.get("result", {}))}]}] if task.get("result") else []
398
532
  }
399
533
  })
400
534
 
@@ -411,974 +545,1285 @@ async def handle_task_cancel(request: A2ARequest) -> JSONResponse:
411
545
  })
412
546
 
413
547
 
414
- async def execute_skill(
415
- skill_id: str,
416
- query: str,
417
- params: Dict[str, Any],
418
- session_id: Optional[str] = None
419
- ) -> Dict[str, Any]:
420
- """Execute the specified skill with enhanced context support."""
421
- # Debug to file
422
- with open("c:/Users/moham/Desktop/Claude Memory/memory-agent/debug.log", "a") as f:
423
- f.write(f"[SKILL DEBUG] execute_skill called with skill_id='{skill_id}'\n")
424
- f.flush()
425
-
426
- if skill_id == "store_memory":
427
- result = await store_memory(
428
- db=db,
429
- embeddings=embeddings,
430
- content=params.get("content", query),
431
- memory_type=params.get("type", "chunk"),
432
- metadata=params.get("metadata"),
433
- session_id=session_id or params.get("session_id"),
434
- # Project context
435
- project_path=params.get("project_path"),
436
- project_name=params.get("project_name"),
437
- project_type=params.get("project_type"),
438
- tech_stack=params.get("tech_stack"),
439
- # Agent context
440
- agent_type=params.get("agent_type"),
441
- skill_used=params.get("skill_used"),
442
- tools_used=params.get("tools_used"),
443
- # Outcome
444
- outcome=params.get("outcome"),
445
- success=params.get("success"),
446
- # Classification
447
- tags=params.get("tags"),
448
- importance=params.get("importance", 5),
449
- confidence=params.get("confidence", 0.5),
450
- # Outcome spectrum
451
- outcome_status=params.get("outcome_status", "pending"),
452
- fixed=params.get("fixed"),
453
- did_not_fix=params.get("did_not_fix"),
454
- caused=params.get("caused")
455
- )
456
- # Broadcast real-time update
457
- print(f"[DEBUG] About to broadcast memory_stored event for memory_id={result.get('memory_id')}")
458
- try:
459
- await broadcast_event(
460
- EventTypes.MEMORY_STORED,
461
- {"memory_id": result.get("memory_id"), "type": params.get("type", "chunk")},
462
- params.get("project_path")
463
- )
464
- print(f"[DEBUG] Broadcast completed successfully")
465
- except Exception as e:
466
- print(f"[DEBUG] Broadcast error: {e}")
467
- return result
548
+ # ============================================================
549
+ # SKILL HANDLER FUNCTIONS
550
+ # ============================================================
551
+ # Each handler receives (query, params, session_id) and returns a dict.
552
+ # Grouped by category for maintainability.
553
+ # ============================================================
468
554
 
469
- elif skill_id == "store_project":
470
- return await store_project(
471
- db=db,
472
- path=params.get("path"),
473
- name=params.get("name"),
474
- project_type=params.get("project_type"),
475
- tech_stack=params.get("tech_stack"),
476
- conventions=params.get("conventions"),
477
- preferences=params.get("preferences")
478
- )
479
555
 
480
- elif skill_id == "store_pattern":
481
- return await store_pattern(
482
- db=db,
483
- embeddings=embeddings,
484
- name=params.get("name"),
485
- solution=params.get("solution"),
486
- problem_type=params.get("problem_type"),
487
- tech_context=params.get("tech_context"),
488
- metadata=params.get("metadata")
489
- )
556
+ # --- Core Memory Skills ---
490
557
 
491
- elif skill_id == "retrieve_memory":
492
- return await retrieve_memory(
493
- db=db,
494
- memory_id=params.get("memory_id"),
495
- memory_type=params.get("type"),
496
- session_id=session_id or params.get("session_id"),
497
- project_path=params.get("project_path"),
498
- limit=params.get("limit", 10)
558
+ async def _handle_store_memory(query, params, session_id):
559
+ result = await store_memory(
560
+ db=db,
561
+ embeddings=embeddings,
562
+ content=params.get("content", query),
563
+ memory_type=params.get("type", "chunk"),
564
+ metadata=params.get("metadata"),
565
+ session_id=session_id or params.get("session_id"),
566
+ project_path=params.get("project_path"),
567
+ project_name=params.get("project_name"),
568
+ project_type=params.get("project_type"),
569
+ tech_stack=params.get("tech_stack"),
570
+ agent_type=params.get("agent_type"),
571
+ skill_used=params.get("skill_used"),
572
+ tools_used=params.get("tools_used"),
573
+ outcome=params.get("outcome"),
574
+ success=params.get("success"),
575
+ tags=params.get("tags"),
576
+ importance=params.get("importance", 5),
577
+ confidence=params.get("confidence", 0.5),
578
+ outcome_status=params.get("outcome_status", "pending"),
579
+ fixed=params.get("fixed"),
580
+ did_not_fix=params.get("did_not_fix"),
581
+ caused=params.get("caused")
582
+ )
583
+ metrics.record_store(
584
+ merged=result.get("action") == "merged",
585
+ has_embedding=result.get("has_embedding", True)
586
+ )
587
+ try:
588
+ await broadcast_event(
589
+ EventTypes.MEMORY_STORED,
590
+ {"memory_id": result.get("memory_id"), "type": params.get("type", "chunk")},
591
+ params.get("project_path")
499
592
  )
593
+ except Exception as e:
594
+ logger.debug(f"Broadcast error: {e}")
595
+ return result
500
596
 
501
- elif skill_id == "semantic_search":
502
- return await semantic_search(
503
- db=db,
504
- embeddings=embeddings,
505
- query=params.get("query", query),
506
- limit=params.get("limit", 10),
507
- memory_type=params.get("type"),
508
- session_id=session_id or params.get("session_id"),
509
- project_path=params.get("project_path"),
510
- agent_type=params.get("agent_type"),
511
- success_only=params.get("success_only", False),
512
- threshold=params.get("threshold", 0.5),
513
- # Outcome spectrum filters
514
- include_failed=params.get("include_failed", False),
515
- include_superseded=params.get("include_superseded", False),
516
- include_unreliable=params.get("include_unreliable", False),
517
- outcome_status=params.get("outcome_status")
518
- )
519
597
 
520
- elif skill_id == "search_patterns":
521
- return await search_patterns(
522
- db=db,
523
- embeddings=embeddings,
524
- query=params.get("query", query),
525
- limit=params.get("limit", 5),
526
- problem_type=params.get("problem_type"),
527
- threshold=params.get("threshold", 0.5)
528
- )
598
+ async def _handle_store_project(query, params, session_id):
599
+ return await store_project(
600
+ db=db,
601
+ path=params.get("path"),
602
+ name=params.get("name"),
603
+ project_type=params.get("project_type"),
604
+ tech_stack=params.get("tech_stack"),
605
+ conventions=params.get("conventions"),
606
+ preferences=params.get("preferences")
607
+ )
529
608
 
530
- elif skill_id == "get_project_context":
531
- return await get_project_context(
532
- db=db,
533
- embeddings=embeddings,
534
- project_path=params.get("project_path"),
535
- query=params.get("query"),
536
- limit=params.get("limit", 10)
537
- )
538
609
 
539
- elif skill_id == "summarize_session":
540
- return await summarize_session(
541
- db=db,
542
- embeddings=embeddings,
543
- session_id=session_id or params.get("session_id", str(uuid.uuid4())),
544
- summary=params.get("summary", query),
545
- key_decisions=params.get("key_decisions"),
546
- code_patterns=params.get("code_patterns"),
547
- metadata=params.get("metadata"),
548
- project_path=params.get("project_path")
549
- )
610
+ async def _handle_store_pattern(query, params, session_id):
611
+ return await store_pattern(
612
+ db=db,
613
+ embeddings=embeddings,
614
+ name=params.get("name"),
615
+ solution=params.get("solution"),
616
+ problem_type=params.get("problem_type"),
617
+ tech_context=params.get("tech_context"),
618
+ metadata=params.get("metadata")
619
+ )
550
620
 
551
- elif skill_id == "auto_summarize_session":
552
- return await auto_summarize_session(
553
- db=db,
554
- embeddings=embeddings,
555
- session_id=session_id or params.get("session_id"),
556
- project_path=params.get("project_path")
557
- )
558
621
 
559
- elif skill_id == "get_session_handoff":
560
- return await get_session_handoff(
561
- db=db,
562
- embeddings=embeddings,
563
- project_path=params.get("project_path"),
564
- include_last_n_sessions=params.get("include_last_n_sessions", 3)
565
- )
622
+ async def _handle_retrieve_memory(query, params, session_id):
623
+ return await retrieve_memory(
624
+ db=db,
625
+ memory_id=params.get("memory_id"),
626
+ memory_type=params.get("type"),
627
+ session_id=session_id or params.get("session_id"),
628
+ project_path=params.get("project_path"),
629
+ limit=params.get("limit", 10)
630
+ )
566
631
 
567
- elif skill_id == "create_diary_entry":
568
- return await create_diary_entry(
569
- db=db,
570
- embeddings=embeddings,
571
- session_id=session_id or params.get("session_id"),
572
- project_path=params.get("project_path"),
573
- user_notes=params.get("user_notes")
574
- )
575
632
 
576
- elif skill_id == "check_session_inactivity":
577
- return await check_session_inactivity(
578
- db=db,
579
- session_id=session_id or params.get("session_id"),
580
- inactivity_threshold_hours=params.get("inactivity_threshold_hours", 4.0)
581
- )
633
+ async def _handle_semantic_search(query, params, session_id):
634
+ result = await semantic_search(
635
+ db=db,
636
+ embeddings=embeddings,
637
+ query=params.get("query", query),
638
+ limit=params.get("limit", 10),
639
+ memory_type=params.get("type"),
640
+ session_id=session_id or params.get("session_id"),
641
+ project_path=params.get("project_path"),
642
+ agent_type=params.get("agent_type"),
643
+ success_only=params.get("success_only", False),
644
+ threshold=params.get("threshold", 0.5),
645
+ include_failed=params.get("include_failed", False),
646
+ include_superseded=params.get("include_superseded", False),
647
+ include_unreliable=params.get("include_unreliable", False),
648
+ outcome_status=params.get("outcome_status"),
649
+ include_graph=params.get("include_graph", True),
650
+ temperature=params.get("temperature")
651
+ )
652
+ metrics.record_search(result.get("count", 0))
653
+ return result
582
654
 
583
- elif skill_id == "get_stats":
584
- return await db.get_stats()
585
655
 
586
- # ============================================================
587
- # TIMELINE SKILLS
588
- # ============================================================
656
+ async def _handle_search_patterns(query, params, session_id):
657
+ result = await search_patterns(
658
+ db=db,
659
+ embeddings=embeddings,
660
+ query=params.get("query", query),
661
+ limit=params.get("limit", 5),
662
+ problem_type=params.get("problem_type"),
663
+ threshold=params.get("threshold", 0.5)
664
+ )
665
+ metrics.record_search(result.get("count", 0))
666
+ return result
589
667
 
590
- elif skill_id == "timeline_log":
591
- result = await timeline_log(
592
- db=db,
593
- embeddings=embeddings,
594
- session_id=params.get("session_id") or session_id or str(uuid.uuid4()),
595
- event_type=params.get("event_type", "observation"),
596
- summary=params.get("summary", query),
597
- details=params.get("details"),
598
- project_path=params.get("project_path"),
599
- parent_event_id=params.get("parent_event_id"),
600
- root_event_id=params.get("root_event_id"),
601
- entities=params.get("entities"),
602
- status=params.get("status", "completed"),
603
- outcome=params.get("outcome"),
604
- confidence=params.get("confidence"),
605
- is_anchor=params.get("is_anchor", False)
606
- )
607
- # Broadcast real-time update
608
- await broadcast_event(
609
- EventTypes.TIMELINE_LOGGED,
610
- {"event_id": result.get("event_id"), "event_type": params.get("event_type", "observation")},
611
- params.get("project_path")
612
- )
613
- return result
614
668
 
615
- elif skill_id == "timeline_log_batch":
616
- # Batch logging - more efficient than multiple timeline_log calls
617
- result = await timeline_log_batch(
618
- db=db,
619
- embeddings=embeddings,
620
- session_id=params.get("session_id") or session_id or str(uuid.uuid4()),
621
- events=params.get("events", []),
622
- project_path=params.get("project_path"),
623
- parent_event_id=params.get("parent_event_id"),
624
- root_event_id=params.get("root_event_id")
625
- )
626
- # Broadcast single update for the batch
627
- if result.get("events_logged", 0) > 0:
628
- await broadcast_event(
629
- EventTypes.TIMELINE_LOGGED,
630
- {
631
- "event_ids": result.get("event_ids", []),
632
- "batch_size": result.get("events_logged", 0),
633
- "event_types": result.get("event_types", {})
634
- },
635
- params.get("project_path")
636
- )
637
- return result
669
+ async def _handle_get_project_context(query, params, session_id):
670
+ return await get_project_context(
671
+ db=db,
672
+ embeddings=embeddings,
673
+ project_path=params.get("project_path"),
674
+ query=params.get("query"),
675
+ limit=params.get("limit", 10)
676
+ )
638
677
 
639
- elif skill_id == "timeline_get":
640
- return await timeline_get(
641
- db=db,
642
- session_id=params.get("session_id") or session_id,
643
- limit=params.get("limit", 20),
644
- event_type=params.get("event_type"),
645
- since_event_id=params.get("since_event_id"),
646
- anchors_only=params.get("anchors_only", False),
647
- include_state=params.get("include_state", True),
648
- include_checkpoint=params.get("include_checkpoint", True)
649
- )
650
678
 
651
- elif skill_id == "timeline_search":
652
- return await timeline_search(
653
- db=db,
654
- embeddings=embeddings,
655
- query=params.get("query", query),
656
- session_id=params.get("session_id") or session_id,
657
- limit=params.get("limit", 10),
658
- threshold=params.get("threshold", 0.5)
659
- )
679
+ # --- Session Skills ---
660
680
 
661
- elif skill_id == "timeline_auto_detect":
662
- return await timeline_auto_detect(
663
- db=db,
664
- embeddings=embeddings,
665
- session_id=params.get("session_id") or session_id or str(uuid.uuid4()),
666
- response_text=params.get("response_text", query),
667
- project_path=params.get("project_path"),
668
- parent_event_id=params.get("parent_event_id")
669
- )
681
+ async def _handle_summarize_session(query, params, session_id):
682
+ return await summarize_session(
683
+ db=db,
684
+ embeddings=embeddings,
685
+ session_id=session_id or params.get("session_id", str(uuid.uuid4())),
686
+ summary=params.get("summary", query),
687
+ key_decisions=params.get("key_decisions"),
688
+ code_patterns=params.get("code_patterns"),
689
+ metadata=params.get("metadata"),
690
+ project_path=params.get("project_path")
691
+ )
670
692
 
671
- elif skill_id == "timeline_chain":
672
- return await timeline_chain(
673
- db=db,
674
- session_id=params.get("session_id") or session_id,
675
- root_event_id=params.get("root_event_id"),
676
- include_details=params.get("include_details", False)
677
- )
678
693
 
679
- # ============================================================
680
- # STATE SKILLS
681
- # ============================================================
694
+ async def _handle_auto_summarize_session(query, params, session_id):
695
+ return await auto_summarize_session(
696
+ db=db,
697
+ embeddings=embeddings,
698
+ session_id=session_id or params.get("session_id"),
699
+ project_path=params.get("project_path")
700
+ )
682
701
 
683
- elif skill_id == "state_get":
684
- return await state_get(
685
- db=db,
686
- session_id=params.get("session_id") or session_id,
687
- project_path=params.get("project_path")
688
- )
689
702
 
690
- elif skill_id == "state_update":
691
- return await state_update(
692
- db=db,
693
- session_id=params.get("session_id") or session_id,
694
- current_goal=params.get("current_goal"),
695
- pending_questions=params.get("pending_questions"),
696
- add_question=params.get("add_question"),
697
- remove_question=params.get("remove_question"),
698
- register_entity=params.get("register_entity"),
699
- entity_registry=params.get("entity_registry"),
700
- add_decision=params.get("add_decision"),
701
- decisions_summary=params.get("decisions_summary")
702
- )
703
+ async def _handle_get_session_handoff(query, params, session_id):
704
+ return await get_session_handoff(
705
+ db=db,
706
+ embeddings=embeddings,
707
+ project_path=params.get("project_path"),
708
+ include_last_n_sessions=params.get("include_last_n_sessions", 3)
709
+ )
703
710
 
704
- elif skill_id == "state_init_session":
705
- return await state_init_session(
706
- db=db,
707
- embeddings=embeddings,
708
- project_path=params.get("project_path")
709
- )
710
711
 
711
- # ============================================================
712
- # CHECKPOINT SKILLS
713
- # ============================================================
712
+ async def _handle_create_diary_entry(query, params, session_id):
713
+ return await create_diary_entry(
714
+ db=db,
715
+ embeddings=embeddings,
716
+ session_id=session_id or params.get("session_id"),
717
+ project_path=params.get("project_path"),
718
+ user_notes=params.get("user_notes")
719
+ )
714
720
 
715
- elif skill_id == "checkpoint_create":
716
- return await checkpoint_create(
717
- db=db,
718
- embeddings=embeddings,
719
- session_id=params.get("session_id") or session_id,
720
- summary=params.get("summary"),
721
- key_facts=params.get("key_facts"),
722
- include_state=params.get("include_state", True)
723
- )
724
721
 
725
- elif skill_id == "checkpoint_load":
726
- return await checkpoint_load(
727
- db=db,
728
- session_id=params.get("session_id") or session_id,
729
- checkpoint_id=params.get("checkpoint_id"),
730
- project_path=params.get("project_path")
731
- )
722
+ async def _handle_check_session_inactivity(query, params, session_id):
723
+ return await check_session_inactivity(
724
+ db=db,
725
+ session_id=session_id or params.get("session_id"),
726
+ inactivity_threshold_hours=params.get("inactivity_threshold_hours", 4.0)
727
+ )
732
728
 
733
- elif skill_id == "checkpoint_list":
734
- return await checkpoint_list(
735
- db=db,
736
- session_id=params.get("session_id") or session_id,
737
- limit=params.get("limit", 10)
738
- )
739
729
 
740
- # ============================================================
741
- # GROUNDING SKILLS (Anti-Hallucination)
742
- # ============================================================
730
+ async def _handle_get_stats(query, params, session_id):
731
+ return await db.get_stats()
743
732
 
744
- elif skill_id == "context_refresh":
745
- return await context_refresh(
746
- db=db,
747
- embeddings=embeddings,
748
- session_id=params.get("session_id") or session_id,
749
- query=params.get("query", query) if query else None,
750
- include_recent_events=params.get("include_recent_events", 10),
751
- include_state=params.get("include_state", True),
752
- include_checkpoint=params.get("include_checkpoint", True),
753
- include_relevant_memories=params.get("include_relevant_memories", True),
754
- check_contradictions=params.get("check_contradictions", True)
755
- )
756
733
 
757
- elif skill_id == "check_contradictions":
758
- return await check_contradictions(
759
- db=db,
760
- embeddings=embeddings,
761
- statement=params.get("statement", query),
762
- session_id=params.get("session_id") or session_id,
763
- scope=params.get("scope", "session")
764
- )
734
+ # --- Timeline Skills ---
765
735
 
766
- elif skill_id == "verify_entity":
767
- return await verify_entity(
768
- db=db,
769
- session_id=params.get("session_id") or session_id,
770
- entity_key=params.get("entity_key"),
771
- entity_type=params.get("entity_type")
772
- )
736
+ async def _handle_timeline_log(query, params, session_id):
737
+ result = await timeline_log(
738
+ db=db,
739
+ embeddings=embeddings,
740
+ session_id=params.get("session_id") or session_id or str(uuid.uuid4()),
741
+ event_type=params.get("event_type", "observation"),
742
+ summary=params.get("summary", query),
743
+ details=params.get("details"),
744
+ project_path=params.get("project_path"),
745
+ parent_event_id=params.get("parent_event_id"),
746
+ root_event_id=params.get("root_event_id"),
747
+ entities=params.get("entities"),
748
+ status=params.get("status", "completed"),
749
+ outcome=params.get("outcome"),
750
+ confidence=params.get("confidence"),
751
+ is_anchor=params.get("is_anchor", False)
752
+ )
753
+ await broadcast_event(
754
+ EventTypes.TIMELINE_LOGGED,
755
+ {"event_id": result.get("event_id"), "event_type": params.get("event_type", "observation")},
756
+ params.get("project_path")
757
+ )
758
+ return result
773
759
 
774
- elif skill_id == "mark_anchor":
775
- result = await mark_anchor(
776
- db=db,
777
- embeddings=embeddings,
778
- session_id=params.get("session_id") or session_id,
779
- fact=params.get("fact", query),
780
- details=params.get("details"),
781
- project_path=params.get("project_path"),
782
- force=params.get("force", False)
783
- )
784
- # Broadcast real-time update
785
- event_type = EventTypes.ANCHOR_CONFLICT if result.get("conflict_detected") else EventTypes.ANCHOR_MARKED
760
+
761
+ async def _handle_timeline_log_batch(query, params, session_id):
762
+ result = await timeline_log_batch(
763
+ db=db,
764
+ embeddings=embeddings,
765
+ session_id=params.get("session_id") or session_id or str(uuid.uuid4()),
766
+ events=params.get("events", []),
767
+ project_path=params.get("project_path"),
768
+ parent_event_id=params.get("parent_event_id"),
769
+ root_event_id=params.get("root_event_id")
770
+ )
771
+ if result.get("events_logged", 0) > 0:
786
772
  await broadcast_event(
787
- event_type,
788
- {"anchor_id": result.get("anchor_id"), "fact": params.get("fact", query)[:100]},
773
+ EventTypes.TIMELINE_LOGGED,
774
+ {
775
+ "event_ids": result.get("event_ids", []),
776
+ "batch_size": result.get("events_logged", 0),
777
+ "event_types": result.get("event_types", {})
778
+ },
789
779
  params.get("project_path")
790
780
  )
791
- return result
781
+ return result
792
782
 
793
- elif skill_id == "get_unresolved_conflicts":
794
- return await get_unresolved_conflicts(
795
- db=db,
796
- session_id=params.get("session_id") or session_id,
797
- project_path=params.get("project_path"),
798
- limit=params.get("limit", 20)
799
- )
800
783
 
801
- elif skill_id == "resolve_conflict":
802
- return await resolve_conflict(
803
- db=db,
804
- embeddings=embeddings,
805
- conflict_id=params.get("conflict_id"),
806
- resolution=params.get("resolution"),
807
- keep_anchor_id=params.get("keep_anchor_id"),
808
- resolved_by=params.get("resolved_by", "user")
809
- )
784
+ async def _handle_timeline_get(query, params, session_id):
785
+ return await timeline_get(
786
+ db=db,
787
+ session_id=params.get("session_id") or session_id,
788
+ limit=params.get("limit", 20),
789
+ event_type=params.get("event_type"),
790
+ since_event_id=params.get("since_event_id"),
791
+ anchors_only=params.get("anchors_only", False),
792
+ include_state=params.get("include_state", True),
793
+ include_checkpoint=params.get("include_checkpoint", True)
794
+ )
810
795
 
811
- elif skill_id == "get_anchor_history":
812
- return await get_anchor_history(
813
- db=db,
814
- anchor_id=params.get("anchor_id"),
815
- session_id=params.get("session_id") or session_id,
816
- limit=params.get("limit", 50)
817
- )
818
796
 
819
- elif skill_id == "auto_resolve_conflicts":
820
- return await auto_resolve_conflicts(
821
- db=db,
822
- embeddings=embeddings,
823
- session_id=params.get("session_id") or session_id
824
- )
797
+ async def _handle_timeline_search(query, params, session_id):
798
+ return await timeline_search(
799
+ db=db,
800
+ embeddings=embeddings,
801
+ query=params.get("query", query),
802
+ session_id=params.get("session_id") or session_id,
803
+ limit=params.get("limit", 10),
804
+ threshold=params.get("threshold", 0.5)
805
+ )
825
806
 
826
- # ============================================================
827
- # SELF-CORRECTING CONFIDENCE SKILLS
828
- # ============================================================
829
807
 
830
- elif skill_id == "memory_worked":
831
- from skills.confidence_tracker import report_solution_outcome
832
- result = await report_solution_outcome(
833
- db=db,
834
- memory_id=params.get("memory_id"),
835
- worked=True,
836
- context=params.get("context")
837
- )
838
- if result.get("success"):
839
- await broadcast_event(
840
- EventTypes.MEMORY_UPDATED,
841
- {
842
- "memory_id": params.get("memory_id"),
843
- "action": "worked",
844
- "new_confidence": result.get("new_confidence"),
845
- "reliability": result.get("reliability")
846
- }
847
- )
848
- return result
808
+ async def _handle_timeline_auto_detect(query, params, session_id):
809
+ return await timeline_auto_detect(
810
+ db=db,
811
+ embeddings=embeddings,
812
+ session_id=params.get("session_id") or session_id or str(uuid.uuid4()),
813
+ response_text=params.get("response_text", query),
814
+ project_path=params.get("project_path"),
815
+ parent_event_id=params.get("parent_event_id")
816
+ )
849
817
 
850
- elif skill_id == "memory_failed":
851
- from skills.confidence_tracker import report_solution_outcome
852
- result = await report_solution_outcome(
853
- db=db,
854
- memory_id=params.get("memory_id"),
855
- worked=False,
856
- context=params.get("context")
857
- )
858
- if result.get("success"):
859
- await broadcast_event(
860
- EventTypes.MEMORY_UPDATED,
861
- {
862
- "memory_id": params.get("memory_id"),
863
- "action": "failed",
864
- "new_confidence": result.get("new_confidence"),
865
- "reliability": result.get("reliability"),
866
- "is_unreliable": result.get("is_unreliable")
867
- }
868
- )
869
- return result
870
818
 
871
- elif skill_id == "get_reliability_stats":
872
- from skills.confidence_tracker import get_reliability_stats
873
- return await get_reliability_stats(
874
- db=db,
875
- memory_id=params.get("memory_id")
876
- )
819
+ async def _handle_timeline_chain(query, params, session_id):
820
+ return await timeline_chain(
821
+ db=db,
822
+ session_id=params.get("session_id") or session_id,
823
+ root_event_id=params.get("root_event_id"),
824
+ include_details=params.get("include_details", False)
825
+ )
877
826
 
878
- elif skill_id == "get_unreliable_memories":
879
- from skills.confidence_tracker import get_unreliable_memories
880
- return await get_unreliable_memories(
881
- db=db,
882
- project_path=params.get("project_path"),
883
- limit=params.get("limit", 50)
884
- )
885
827
 
886
- elif skill_id == "reset_memory_reliability":
887
- from skills.confidence_tracker import reset_memory_reliability
888
- return await reset_memory_reliability(
889
- db=db,
890
- memory_id=params.get("memory_id"),
891
- new_confidence=params.get("confidence", 0.5)
892
- )
828
+ # --- State Skills ---
893
829
 
894
- # ============================================================
895
- # CLAUDE.MD MANAGEMENT SKILLS
896
- # ============================================================
830
+ async def _handle_state_get(query, params, session_id):
831
+ return await state_get(
832
+ db=db,
833
+ session_id=params.get("session_id") or session_id,
834
+ project_path=params.get("project_path")
835
+ )
897
836
 
898
- elif skill_id == "claude_md_read":
899
- return await claude_md_read(
900
- section=params.get("section")
901
- )
902
837
 
903
- elif skill_id == "claude_md_add_section":
904
- return await claude_md_add_section(
905
- section_name=params.get("section_name"),
906
- content=params.get("content", query),
907
- position=params.get("position", "end")
908
- )
838
+ async def _handle_state_update(query, params, session_id):
839
+ return await state_update(
840
+ db=db,
841
+ session_id=params.get("session_id") or session_id,
842
+ current_goal=params.get("current_goal"),
843
+ pending_questions=params.get("pending_questions"),
844
+ add_question=params.get("add_question"),
845
+ remove_question=params.get("remove_question"),
846
+ register_entity=params.get("register_entity"),
847
+ entity_registry=params.get("entity_registry"),
848
+ add_decision=params.get("add_decision"),
849
+ decisions_summary=params.get("decisions_summary")
850
+ )
909
851
 
910
- elif skill_id == "claude_md_update_section":
911
- return await claude_md_update_section(
912
- section_name=params.get("section_name"),
913
- content=params.get("content", query),
914
- mode=params.get("mode", "replace")
915
- )
916
852
 
917
- elif skill_id == "claude_md_add_instruction":
918
- return await claude_md_add_instruction(
919
- section_name=params.get("section_name"),
920
- instruction=params.get("instruction", query),
921
- bullet_style=params.get("bullet_style", "-")
922
- )
853
+ async def _handle_state_init_session(query, params, session_id):
854
+ return await state_init_session(
855
+ db=db,
856
+ embeddings=embeddings,
857
+ project_path=params.get("project_path")
858
+ )
923
859
 
924
- elif skill_id == "claude_md_list_sections":
925
- return await claude_md_list_sections()
926
860
 
927
- elif skill_id == "claude_md_suggest":
928
- return await claude_md_suggest_from_session(
929
- db=db,
930
- session_id=params.get("session_id") or session_id,
931
- min_importance=params.get("min_importance", 7)
932
- )
861
+ # --- Checkpoint Skills ---
933
862
 
934
- # ============================================================
935
- # VERIFICATION SKILLS (Best-of-N, Quote Extraction)
936
- # ============================================================
863
+ async def _handle_checkpoint_create(query, params, session_id):
864
+ return await checkpoint_create(
865
+ db=db,
866
+ embeddings=embeddings,
867
+ session_id=params.get("session_id") or session_id,
868
+ summary=params.get("summary"),
869
+ key_facts=params.get("key_facts"),
870
+ include_state=params.get("include_state", True)
871
+ )
937
872
 
938
- elif skill_id == "best_of_n_verify":
939
- return await best_of_n_verify(
940
- query=params.get("query", query),
941
- n=params.get("n", 3),
942
- context=params.get("context"),
943
- threshold=params.get("threshold", 0.7)
944
- )
945
873
 
946
- elif skill_id == "extract_quotes":
947
- return await extract_quotes(
948
- document=params.get("document", ""),
949
- query=params.get("query", query),
950
- max_quotes=params.get("max_quotes", 5),
951
- min_length=params.get("min_length", 20)
952
- )
874
+ async def _handle_checkpoint_load(query, params, session_id):
875
+ return await checkpoint_load(
876
+ db=db,
877
+ session_id=params.get("session_id") or session_id,
878
+ checkpoint_id=params.get("checkpoint_id"),
879
+ project_path=params.get("project_path")
880
+ )
953
881
 
954
- elif skill_id == "require_grounding":
955
- return await require_grounding(
956
- db=db,
957
- session_id=params.get("session_id") or session_id,
958
- statement=params.get("statement", query),
959
- source_type=params.get("source_type", "any")
960
- )
961
882
 
962
- # ============================================================
963
- # CROSS-SESSION LEARNING SKILLS
964
- # ============================================================
883
+ async def _handle_checkpoint_list(query, params, session_id):
884
+ return await checkpoint_list(
885
+ db=db,
886
+ session_id=params.get("session_id") or session_id,
887
+ limit=params.get("limit", 10)
888
+ )
965
889
 
966
- elif skill_id == "run_aggregation":
967
- return await run_aggregation(
968
- db=db,
969
- embeddings=embeddings,
970
- days_back=params.get("days_back", 30)
971
- )
972
890
 
973
- elif skill_id == "get_insights":
974
- return await get_insights(
975
- db=db,
976
- embeddings=embeddings,
977
- insight_type=params.get("insight_type"),
978
- project_path=params.get("project_path"),
979
- min_confidence=params.get("min_confidence", 0.5),
980
- limit=params.get("limit", 10)
891
+ # --- Grounding Skills (Anti-Hallucination) ---
892
+
893
+ async def _handle_context_refresh(query, params, session_id):
894
+ return await context_refresh(
895
+ db=db,
896
+ embeddings=embeddings,
897
+ session_id=params.get("session_id") or session_id,
898
+ query=params.get("query", query) if query else None,
899
+ include_recent_events=params.get("include_recent_events", 10),
900
+ include_state=params.get("include_state", True),
901
+ include_checkpoint=params.get("include_checkpoint", True),
902
+ include_relevant_memories=params.get("include_relevant_memories", True),
903
+ check_contradictions=params.get("check_contradictions", True)
904
+ )
905
+
906
+
907
+ async def _handle_check_contradictions(query, params, session_id):
908
+ return await check_contradictions(
909
+ db=db,
910
+ embeddings=embeddings,
911
+ statement=params.get("statement", query),
912
+ session_id=params.get("session_id") or session_id,
913
+ scope=params.get("scope", "session")
914
+ )
915
+
916
+
917
+ async def _handle_verify_entity(query, params, session_id):
918
+ return await verify_entity(
919
+ db=db,
920
+ session_id=params.get("session_id") or session_id,
921
+ entity_key=params.get("entity_key"),
922
+ entity_type=params.get("entity_type")
923
+ )
924
+
925
+
926
+ async def _handle_mark_anchor(query, params, session_id):
927
+ result = await mark_anchor(
928
+ db=db,
929
+ embeddings=embeddings,
930
+ session_id=params.get("session_id") or session_id,
931
+ fact=params.get("fact", query),
932
+ details=params.get("details"),
933
+ project_path=params.get("project_path"),
934
+ force=params.get("force", False)
935
+ )
936
+ event_type = EventTypes.ANCHOR_CONFLICT if result.get("conflict_detected") else EventTypes.ANCHOR_MARKED
937
+ await broadcast_event(
938
+ event_type,
939
+ {"anchor_id": result.get("anchor_id"), "fact": params.get("fact", query)[:100]},
940
+ params.get("project_path")
941
+ )
942
+ return result
943
+
944
+
945
+ async def _handle_get_unresolved_conflicts(query, params, session_id):
946
+ return await get_unresolved_conflicts(
947
+ db=db,
948
+ session_id=params.get("session_id") or session_id,
949
+ project_path=params.get("project_path"),
950
+ limit=params.get("limit", 20)
951
+ )
952
+
953
+
954
+ async def _handle_resolve_conflict(query, params, session_id):
955
+ return await resolve_conflict(
956
+ db=db,
957
+ embeddings=embeddings,
958
+ conflict_id=params.get("conflict_id"),
959
+ resolution=params.get("resolution"),
960
+ keep_anchor_id=params.get("keep_anchor_id"),
961
+ resolved_by=params.get("resolved_by", "user")
962
+ )
963
+
964
+
965
+ async def _handle_get_anchor_history(query, params, session_id):
966
+ return await get_anchor_history(
967
+ db=db,
968
+ anchor_id=params.get("anchor_id"),
969
+ session_id=params.get("session_id") or session_id,
970
+ limit=params.get("limit", 50)
971
+ )
972
+
973
+
974
+ async def _handle_auto_resolve_conflicts(query, params, session_id):
975
+ return await auto_resolve_conflicts(
976
+ db=db,
977
+ embeddings=embeddings,
978
+ session_id=params.get("session_id") or session_id
979
+ )
980
+
981
+
982
+ # --- Self-Correcting Confidence Skills ---
983
+
984
+ async def _handle_memory_worked(query, params, session_id):
985
+ from skills.confidence_tracker import report_solution_outcome
986
+ result = await report_solution_outcome(
987
+ db=db,
988
+ memory_id=params.get("memory_id"),
989
+ worked=True,
990
+ context=params.get("context")
991
+ )
992
+ if result.get("success"):
993
+ await broadcast_event(
994
+ EventTypes.MEMORY_UPDATED,
995
+ {
996
+ "memory_id": params.get("memory_id"),
997
+ "action": "worked",
998
+ "new_confidence": result.get("new_confidence"),
999
+ "reliability": result.get("reliability")
1000
+ }
981
1001
  )
1002
+ return result
982
1003
 
983
- elif skill_id == "suggest_improvements":
984
- return await suggest_improvements(
985
- db=db,
986
- embeddings=embeddings,
987
- min_confidence=params.get("min_confidence", 0.7)
1004
+
1005
+ async def _handle_memory_failed(query, params, session_id):
1006
+ from skills.confidence_tracker import report_solution_outcome
1007
+ result = await report_solution_outcome(
1008
+ db=db,
1009
+ memory_id=params.get("memory_id"),
1010
+ worked=False,
1011
+ context=params.get("context")
1012
+ )
1013
+ if result.get("success"):
1014
+ await broadcast_event(
1015
+ EventTypes.MEMORY_UPDATED,
1016
+ {
1017
+ "memory_id": params.get("memory_id"),
1018
+ "action": "failed",
1019
+ "new_confidence": result.get("new_confidence"),
1020
+ "reliability": result.get("reliability"),
1021
+ "is_unreliable": result.get("is_unreliable")
1022
+ }
988
1023
  )
1024
+ return result
989
1025
 
990
- elif skill_id == "record_insight_feedback":
991
- return await record_insight_feedback(
992
- db=db,
993
- embeddings=embeddings,
994
- insight_id=params.get("insight_id"),
995
- helpful=params.get("helpful", True),
996
- session_id=session_id or params.get("session_id"),
997
- comment=params.get("comment")
1026
+
1027
+ async def _handle_get_reliability_stats(query, params, session_id):
1028
+ from skills.confidence_tracker import get_reliability_stats as _get_reliability_stats
1029
+ return await _get_reliability_stats(
1030
+ db=db,
1031
+ memory_id=params.get("memory_id")
1032
+ )
1033
+
1034
+
1035
+ async def _handle_get_unreliable_memories(query, params, session_id):
1036
+ from skills.confidence_tracker import get_unreliable_memories as _get_unreliable_memories
1037
+ return await _get_unreliable_memories(
1038
+ db=db,
1039
+ project_path=params.get("project_path"),
1040
+ limit=params.get("limit", 50)
1041
+ )
1042
+
1043
+
1044
+ async def _handle_reset_memory_reliability(query, params, session_id):
1045
+ from skills.confidence_tracker import reset_memory_reliability as _reset_memory_reliability
1046
+ return await _reset_memory_reliability(
1047
+ db=db,
1048
+ memory_id=params.get("memory_id"),
1049
+ new_confidence=params.get("confidence", 0.5)
1050
+ )
1051
+
1052
+
1053
+ # --- CLAUDE.MD Management Skills ---
1054
+
1055
+ async def _handle_claude_md_read(query, params, session_id):
1056
+ return await claude_md_read(
1057
+ section=params.get("section")
1058
+ )
1059
+
1060
+
1061
+ async def _handle_claude_md_add_section(query, params, session_id):
1062
+ return await claude_md_add_section(
1063
+ section_name=params.get("section_name"),
1064
+ content=params.get("content", query),
1065
+ position=params.get("position", "end")
1066
+ )
1067
+
1068
+
1069
+ async def _handle_claude_md_update_section(query, params, session_id):
1070
+ return await claude_md_update_section(
1071
+ section_name=params.get("section_name"),
1072
+ content=params.get("content", query),
1073
+ mode=params.get("mode", "replace")
1074
+ )
1075
+
1076
+
1077
+ async def _handle_claude_md_add_instruction(query, params, session_id):
1078
+ return await claude_md_add_instruction(
1079
+ section_name=params.get("section_name"),
1080
+ instruction=params.get("instruction", query),
1081
+ bullet_style=params.get("bullet_style", "-")
1082
+ )
1083
+
1084
+
1085
+ async def _handle_claude_md_list_sections(query, params, session_id):
1086
+ return await claude_md_list_sections()
1087
+
1088
+
1089
+ async def _handle_claude_md_suggest(query, params, session_id):
1090
+ return await claude_md_suggest_from_session(
1091
+ db=db,
1092
+ session_id=params.get("session_id") or session_id,
1093
+ min_importance=params.get("min_importance", 7)
1094
+ )
1095
+
1096
+
1097
+ # --- Verification Skills ---
1098
+
1099
+ async def _handle_best_of_n_verify(query, params, session_id):
1100
+ return await best_of_n_verify(
1101
+ query=params.get("query", query),
1102
+ n=params.get("n", 3),
1103
+ context=params.get("context"),
1104
+ threshold=params.get("threshold", 0.7)
1105
+ )
1106
+
1107
+
1108
+ async def _handle_extract_quotes(query, params, session_id):
1109
+ return await extract_quotes(
1110
+ document=params.get("document", ""),
1111
+ query=params.get("query", query),
1112
+ max_quotes=params.get("max_quotes", 5),
1113
+ min_length=params.get("min_length", 20)
1114
+ )
1115
+
1116
+
1117
+ async def _handle_require_grounding(query, params, session_id):
1118
+ return await require_grounding(
1119
+ db=db,
1120
+ session_id=params.get("session_id") or session_id,
1121
+ statement=params.get("statement", query),
1122
+ source_type=params.get("source_type", "any")
1123
+ )
1124
+
1125
+
1126
+ # --- Cross-Session Learning Skills ---
1127
+
1128
+ async def _handle_run_aggregation(query, params, session_id):
1129
+ return await run_aggregation(
1130
+ db=db,
1131
+ embeddings=embeddings,
1132
+ days_back=params.get("days_back", 30)
1133
+ )
1134
+
1135
+
1136
+ async def _handle_get_insights(query, params, session_id):
1137
+ return await get_insights(
1138
+ db=db,
1139
+ embeddings=embeddings,
1140
+ insight_type=params.get("insight_type"),
1141
+ project_path=params.get("project_path"),
1142
+ min_confidence=params.get("min_confidence", 0.5),
1143
+ limit=params.get("limit", 10)
1144
+ )
1145
+
1146
+
1147
+ async def _handle_suggest_improvements(query, params, session_id):
1148
+ return await suggest_improvements(
1149
+ db=db,
1150
+ embeddings=embeddings,
1151
+ min_confidence=params.get("min_confidence", 0.7)
1152
+ )
1153
+
1154
+
1155
+ async def _handle_record_insight_feedback(query, params, session_id):
1156
+ return await record_insight_feedback(
1157
+ db=db,
1158
+ embeddings=embeddings,
1159
+ insight_id=params.get("insight_id"),
1160
+ helpful=params.get("helpful", True),
1161
+ session_id=session_id or params.get("session_id"),
1162
+ comment=params.get("comment")
1163
+ )
1164
+
1165
+
1166
+ async def _handle_mark_insight_applied(query, params, session_id):
1167
+ return await mark_insight_applied(
1168
+ db=db,
1169
+ embeddings=embeddings,
1170
+ insight_id=params.get("insight_id")
1171
+ )
1172
+
1173
+
1174
+ async def _handle_get_project_insights(query, params, session_id):
1175
+ return await get_project_insights(
1176
+ db=db,
1177
+ embeddings=embeddings,
1178
+ project_path=params.get("project_path"),
1179
+ include_global=params.get("include_global", True),
1180
+ limit=params.get("limit", 10)
1181
+ )
1182
+
1183
+
1184
+ # --- Memory Cleanup Skills ---
1185
+
1186
+ async def _handle_memory_cleanup(query, params, session_id):
1187
+ result = await memory_cleanup(
1188
+ db=db,
1189
+ embeddings=embeddings,
1190
+ project_path=params.get("project_path"),
1191
+ dry_run=params.get("dry_run", True)
1192
+ )
1193
+ if not params.get("dry_run", True):
1194
+ await broadcast_event(
1195
+ EventTypes.CLEANUP_COMPLETED,
1196
+ {"archived": result.get("total_archived", 0), "deleted": result.get("total_deleted", 0)},
1197
+ params.get("project_path")
998
1198
  )
1199
+ return result
999
1200
 
1000
- elif skill_id == "mark_insight_applied":
1001
- return await mark_insight_applied(
1002
- db=db,
1003
- embeddings=embeddings,
1004
- insight_id=params.get("insight_id")
1201
+
1202
+ async def _handle_get_archived_memories(query, params, session_id):
1203
+ return await get_archived_memories(
1204
+ db=db,
1205
+ embeddings=embeddings,
1206
+ project_path=params.get("project_path"),
1207
+ reason=params.get("reason"),
1208
+ limit=params.get("limit", 50)
1209
+ )
1210
+
1211
+
1212
+ async def _handle_restore_memory(query, params, session_id):
1213
+ return await restore_memory(
1214
+ db=db,
1215
+ embeddings=embeddings,
1216
+ archive_id=params.get("archive_id")
1217
+ )
1218
+
1219
+
1220
+ async def _handle_get_cleanup_config(query, params, session_id):
1221
+ return await get_cleanup_config(
1222
+ db=db,
1223
+ embeddings=embeddings,
1224
+ project_path=params.get("project_path")
1225
+ )
1226
+
1227
+
1228
+ async def _handle_set_cleanup_config(query, params, session_id):
1229
+ return await set_cleanup_config(
1230
+ db=db,
1231
+ embeddings=embeddings,
1232
+ project_path=params.get("project_path"),
1233
+ retention_days=params.get("retention_days"),
1234
+ min_relevance_score=params.get("min_relevance_score"),
1235
+ keep_high_importance=params.get("keep_high_importance"),
1236
+ importance_threshold=params.get("importance_threshold"),
1237
+ dedup_enabled=params.get("dedup_enabled"),
1238
+ dedup_threshold=params.get("dedup_threshold"),
1239
+ archive_before_delete=params.get("archive_before_delete"),
1240
+ auto_cleanup_enabled=params.get("auto_cleanup_enabled")
1241
+ )
1242
+
1243
+
1244
+ async def _handle_get_cleanup_stats(query, params, session_id):
1245
+ return await get_cleanup_stats(db=db, embeddings=embeddings)
1246
+
1247
+
1248
+ async def _handle_purge_expired_archives(query, params, session_id):
1249
+ return await purge_expired_archives(db=db, embeddings=embeddings)
1250
+
1251
+
1252
+ # --- Admin Skills (Embedding Model Management) ---
1253
+
1254
+ async def _handle_get_embedding_status(query, params, session_id):
1255
+ return await get_embedding_status(db=db, embeddings=embeddings)
1256
+
1257
+
1258
+ async def _handle_switch_embedding_model(query, params, session_id):
1259
+ return await switch_embedding_model(
1260
+ db=db,
1261
+ embeddings=embeddings,
1262
+ model=params.get("model", "nomic-embed-text"),
1263
+ reindex_existing=params.get("reindex_existing", False)
1264
+ )
1265
+
1266
+
1267
+ async def _handle_reindex_memories(query, params, session_id):
1268
+ return await reindex_memories(
1269
+ db=db,
1270
+ embeddings=embeddings,
1271
+ model=params.get("model"),
1272
+ project_path=params.get("project_path"),
1273
+ batch_size=params.get("batch_size", 10),
1274
+ dry_run=params.get("dry_run", False)
1275
+ )
1276
+
1277
+
1278
+ async def _handle_get_reindex_progress(query, params, session_id):
1279
+ return await get_reindex_progress(db=db, embeddings=embeddings)
1280
+
1281
+
1282
+ async def _handle_cancel_reindex(query, params, session_id):
1283
+ return await cancel_reindex(db=db, embeddings=embeddings)
1284
+
1285
+
1286
+ async def _handle_get_model_info(query, params, session_id):
1287
+ return await get_model_info(
1288
+ db=db,
1289
+ embeddings=embeddings,
1290
+ model=params.get("model")
1291
+ )
1292
+
1293
+
1294
+ async def _handle_get_system_stats(query, params, session_id):
1295
+ return await get_system_stats(db=db, embeddings=embeddings)
1296
+
1297
+
1298
+ # --- MoltBot-Inspired Skills (Human-Readable Transparency) ---
1299
+
1300
+ async def _handle_daily_log_append(query, params, session_id):
1301
+ from services.daily_log import append_entry
1302
+ return await append_entry(
1303
+ project_path=params.get("project_path"),
1304
+ content=params.get("content", query),
1305
+ entry_type=params.get("entry_type", "note"),
1306
+ session_id=session_id or params.get("session_id")
1307
+ )
1308
+
1309
+
1310
+ async def _handle_daily_log_append_session(query, params, session_id):
1311
+ from services.daily_log import append_session_summary
1312
+ return await append_session_summary(
1313
+ project_path=params.get("project_path"),
1314
+ session_id=session_id or params.get("session_id"),
1315
+ decisions=params.get("decisions"),
1316
+ accomplishments=params.get("accomplishments"),
1317
+ notes=params.get("notes"),
1318
+ errors_solved=params.get("errors_solved")
1319
+ )
1320
+
1321
+
1322
+ async def _handle_daily_log_read(query, params, session_id):
1323
+ from services.daily_log import load_recent_logs
1324
+ return await load_recent_logs(
1325
+ project_path=params.get("project_path"),
1326
+ days=params.get("days", 2),
1327
+ max_chars=params.get("max_chars", 8000)
1328
+ )
1329
+
1330
+
1331
+ async def _handle_daily_log_highlights(query, params, session_id):
1332
+ from services.daily_log import get_today_highlights
1333
+ return await get_today_highlights(
1334
+ project_path=params.get("project_path"),
1335
+ max_entries=params.get("max_entries", 10)
1336
+ )
1337
+
1338
+
1339
+ async def _handle_daily_log_list(query, params, session_id):
1340
+ from services.daily_log import list_logs
1341
+ return await list_logs(
1342
+ project_path=params.get("project_path"),
1343
+ limit=params.get("limit", 30)
1344
+ )
1345
+
1346
+
1347
+ async def _handle_sync_memory_md(query, params, session_id):
1348
+ from services.memory_md_sync import sync_to_memory_md
1349
+ return await sync_to_memory_md(
1350
+ db=db,
1351
+ project_path=params.get("project_path"),
1352
+ min_importance=params.get("min_importance", 7),
1353
+ min_pattern_success=params.get("min_pattern_success", 3)
1354
+ )
1355
+
1356
+
1357
+ async def _handle_read_memory_md(query, params, session_id):
1358
+ from services.memory_md_sync import read_memory_md
1359
+ return await read_memory_md(
1360
+ project_path=params.get("project_path")
1361
+ )
1362
+
1363
+
1364
+ async def _handle_get_memory_md_summary(query, params, session_id):
1365
+ from services.memory_md_sync import get_memory_md_summary
1366
+ return await get_memory_md_summary(
1367
+ project_path=params.get("project_path")
1368
+ )
1369
+
1370
+
1371
+ async def _handle_add_memory_md_fact(query, params, session_id):
1372
+ from services.memory_md_sync import add_fact
1373
+ return await add_fact(
1374
+ project_path=params.get("project_path"),
1375
+ fact=params.get("fact", query),
1376
+ section=params.get("section", "anchors")
1377
+ )
1378
+
1379
+
1380
+ async def _handle_check_flush_needed(query, params, session_id):
1381
+ from services.compaction_flush import check_flush_needed as _check_flush_needed
1382
+ return await _check_flush_needed(
1383
+ db=db,
1384
+ session_id=session_id or params.get("session_id"),
1385
+ event_threshold=params.get("event_threshold", 50),
1386
+ time_threshold_minutes=params.get("time_threshold_minutes", 30)
1387
+ )
1388
+
1389
+
1390
+ async def _handle_pre_compaction_flush(query, params, session_id):
1391
+ from services.compaction_flush import execute_flush
1392
+ return await execute_flush(
1393
+ db=db,
1394
+ project_path=params.get("project_path"),
1395
+ session_id=session_id or params.get("session_id")
1396
+ )
1397
+
1398
+
1399
+ async def _handle_list_flushes(query, params, session_id):
1400
+ from services.compaction_flush import list_flushes as _list_flushes
1401
+ return await _list_flushes(
1402
+ project_path=params.get("project_path"),
1403
+ limit=params.get("limit", 20)
1404
+ )
1405
+
1406
+
1407
+ async def _handle_read_flush(query, params, session_id):
1408
+ from services.compaction_flush import read_flush as _read_flush
1409
+ return await _read_flush(
1410
+ project_path=params.get("project_path"),
1411
+ filename=params.get("filename")
1412
+ )
1413
+
1414
+
1415
+ # --- Outcome Spectrum Skills ---
1416
+
1417
+ async def _handle_update_memory_outcome(query, params, session_id):
1418
+ result = await db.update_memory_outcome(
1419
+ memory_id=params.get("memory_id"),
1420
+ outcome_status=params.get("outcome_status"),
1421
+ fixed=params.get("fixed"),
1422
+ did_not_fix=params.get("did_not_fix"),
1423
+ caused=params.get("caused"),
1424
+ superseded_by=params.get("superseded_by")
1425
+ )
1426
+ if result.get("success"):
1427
+ await broadcast_event(
1428
+ EventTypes.MEMORY_UPDATED,
1429
+ {
1430
+ "memory_id": params.get("memory_id"),
1431
+ "outcome_status": params.get("outcome_status"),
1432
+ "action": "outcome_updated"
1433
+ }
1005
1434
  )
1435
+ return result
1006
1436
 
1007
- elif skill_id == "get_project_insights":
1008
- return await get_project_insights(
1009
- db=db,
1010
- embeddings=embeddings,
1011
- project_path=params.get("project_path"),
1012
- include_global=params.get("include_global", True),
1013
- limit=params.get("limit", 10)
1437
+
1438
+ async def _handle_supersede_memory(query, params, session_id):
1439
+ result = await db.supersede_memory(
1440
+ old_memory_id=params.get("old_memory_id"),
1441
+ new_memory_id=params.get("new_memory_id"),
1442
+ reason=params.get("reason")
1443
+ )
1444
+ if result.get("success"):
1445
+ await broadcast_event(
1446
+ EventTypes.MEMORY_UPDATED,
1447
+ {
1448
+ "old_memory_id": params.get("old_memory_id"),
1449
+ "new_memory_id": params.get("new_memory_id"),
1450
+ "action": "superseded"
1451
+ }
1014
1452
  )
1453
+ return result
1015
1454
 
1016
- # ============================================================
1017
- # MEMORY CLEANUP SKILLS
1018
- # ============================================================
1019
1455
 
1020
- elif skill_id == "memory_cleanup":
1021
- result = await memory_cleanup(
1022
- db=db,
1023
- embeddings=embeddings,
1024
- project_path=params.get("project_path"),
1025
- dry_run=params.get("dry_run", True)
1026
- )
1027
- # Broadcast real-time update (only for actual cleanup, not dry run)
1028
- if not params.get("dry_run", True):
1029
- await broadcast_event(
1030
- EventTypes.CLEANUP_COMPLETED,
1031
- {"archived": result.get("total_archived", 0), "deleted": result.get("total_deleted", 0)},
1032
- params.get("project_path")
1033
- )
1034
- return result
1456
+ async def _handle_get_superseding_memory(query, params, session_id):
1457
+ superseding = await db.get_superseding_memory(
1458
+ memory_id=params.get("memory_id")
1459
+ )
1460
+ return {
1461
+ "success": True,
1462
+ "superseded": superseding is not None,
1463
+ "superseding_memory": superseding
1464
+ }
1465
+
1466
+
1467
+ # --- Curator Agent Skills ---
1468
+
1469
+ async def _handle_curator_explore(query, params, session_id):
1470
+ from skills.curator import curator_explore as _curator_explore
1471
+ return await _curator_explore(
1472
+ db=db,
1473
+ embeddings=embeddings,
1474
+ start_node_id=params.get("start_node_id"),
1475
+ max_depth=params.get("max_depth", 3),
1476
+ mode=params.get("mode", "bfs"),
1477
+ relationship_filter=params.get("relationship_filter")
1478
+ )
1479
+
1035
1480
 
1036
- elif skill_id == "get_archived_memories":
1037
- return await get_archived_memories(
1038
- db=db,
1039
- embeddings=embeddings,
1040
- project_path=params.get("project_path"),
1041
- reason=params.get("reason"),
1042
- limit=params.get("limit", 50)
1043
- )
1481
+ async def _handle_curator_find_duplicates(query, params, session_id):
1482
+ from skills.curator import curator_find_duplicates as _curator_find_duplicates
1483
+ return await _curator_find_duplicates(
1484
+ db=db,
1485
+ embeddings=embeddings,
1486
+ project_path=params.get("project_path"),
1487
+ similarity_threshold=params.get("similarity_threshold", 0.92),
1488
+ limit=params.get("limit", 50)
1489
+ )
1044
1490
 
1045
- elif skill_id == "restore_memory":
1046
- return await restore_memory(
1047
- db=db,
1048
- embeddings=embeddings,
1049
- archive_id=params.get("archive_id")
1050
- )
1051
1491
 
1052
- elif skill_id == "get_cleanup_config":
1053
- return await get_cleanup_config(
1054
- db=db,
1055
- embeddings=embeddings,
1056
- project_path=params.get("project_path")
1057
- )
1492
+ async def _handle_curator_suggest_links(query, params, session_id):
1493
+ from skills.curator import curator_suggest_links as _curator_suggest_links
1494
+ return await _curator_suggest_links(
1495
+ db=db,
1496
+ embeddings=embeddings,
1497
+ memory_id=params.get("memory_id"),
1498
+ project_path=params.get("project_path"),
1499
+ similarity_threshold=params.get("similarity_threshold", 0.7),
1500
+ limit=params.get("limit", 20)
1501
+ )
1058
1502
 
1059
- elif skill_id == "set_cleanup_config":
1060
- return await set_cleanup_config(
1061
- db=db,
1062
- embeddings=embeddings,
1063
- project_path=params.get("project_path"),
1064
- retention_days=params.get("retention_days"),
1065
- min_relevance_score=params.get("min_relevance_score"),
1066
- keep_high_importance=params.get("keep_high_importance"),
1067
- importance_threshold=params.get("importance_threshold"),
1068
- dedup_enabled=params.get("dedup_enabled"),
1069
- dedup_threshold=params.get("dedup_threshold"),
1070
- archive_before_delete=params.get("archive_before_delete"),
1071
- auto_cleanup_enabled=params.get("auto_cleanup_enabled")
1072
- )
1073
1503
 
1074
- elif skill_id == "get_cleanup_stats":
1075
- return await get_cleanup_stats(db=db, embeddings=embeddings)
1504
+ async def _handle_curator_merge(query, params, session_id):
1505
+ from skills.curator import curator_merge as _curator_merge
1506
+ return await _curator_merge(
1507
+ db=db,
1508
+ embeddings=embeddings,
1509
+ keep_id=params.get("keep_id"),
1510
+ remove_ids=params.get("remove_ids", []),
1511
+ merge_content=params.get("merge_content", False)
1512
+ )
1076
1513
 
1077
- elif skill_id == "purge_expired_archives":
1078
- return await purge_expired_archives(db=db, embeddings=embeddings)
1079
1514
 
1080
- # ============================================================
1081
- # ADMIN SKILLS (Embedding Model Management)
1082
- # ============================================================
1515
+ async def _handle_curator_get_summary(query, params, session_id):
1516
+ from skills.curator import curator_get_summary as _curator_get_summary
1517
+ return await _curator_get_summary(
1518
+ db=db,
1519
+ embeddings=embeddings,
1520
+ query=params.get("query", query),
1521
+ project_path=params.get("project_path"),
1522
+ max_memories=params.get("max_memories", 10),
1523
+ include_graph=params.get("include_graph", True)
1524
+ )
1083
1525
 
1084
- elif skill_id == "get_embedding_status":
1085
- return await get_embedding_status(db=db, embeddings=embeddings)
1086
1526
 
1087
- elif skill_id == "switch_embedding_model":
1088
- return await switch_embedding_model(
1089
- db=db,
1090
- embeddings=embeddings,
1091
- model=params.get("model", "nomic-embed-text"),
1092
- reindex_existing=params.get("reindex_existing", False)
1093
- )
1527
+ async def _handle_curator_run_maintenance(query, params, session_id):
1528
+ from skills.curator import curator_run_maintenance as _curator_run_maintenance
1529
+ return await _curator_run_maintenance(
1530
+ db=db,
1531
+ embeddings=embeddings,
1532
+ project_path=params.get("project_path"),
1533
+ tasks=params.get("tasks")
1534
+ )
1094
1535
 
1095
- elif skill_id == "reindex_memories":
1096
- return await reindex_memories(
1097
- db=db,
1098
- embeddings=embeddings,
1099
- model=params.get("model"),
1100
- project_path=params.get("project_path"),
1101
- batch_size=params.get("batch_size", 10),
1102
- dry_run=params.get("dry_run", False)
1103
- )
1104
1536
 
1105
- elif skill_id == "get_reindex_progress":
1106
- return await get_reindex_progress(db=db, embeddings=embeddings)
1537
+ async def _handle_curator_get_report(query, params, session_id):
1538
+ from skills.curator import curator_get_report as _curator_get_report
1539
+ return await _curator_get_report(
1540
+ db=db,
1541
+ embeddings=embeddings,
1542
+ project_path=params.get("project_path")
1543
+ )
1107
1544
 
1108
- elif skill_id == "cancel_reindex":
1109
- return await cancel_reindex(db=db, embeddings=embeddings)
1110
1545
 
1111
- elif skill_id == "get_model_info":
1112
- return await get_model_info(
1113
- db=db,
1114
- embeddings=embeddings,
1115
- model=params.get("model")
1116
- )
1546
+ async def _handle_curator_get_status(query, params, session_id):
1547
+ from skills.curator import curator_get_status as _curator_get_status
1548
+ return await _curator_get_status(
1549
+ db=db,
1550
+ embeddings=embeddings
1551
+ )
1117
1552
 
1118
- elif skill_id == "get_system_stats":
1119
- return await get_system_stats(db=db, embeddings=embeddings)
1120
1553
 
1121
- # ============================================================
1122
- # MOLTBOT-INSPIRED SKILLS (Human-Readable Transparency)
1123
- # ============================================================
1554
+ async def _handle_curator_score_quality(query, params, session_id):
1555
+ from skills.curator import curator_score_quality as _curator_score_quality
1556
+ return await _curator_score_quality(
1557
+ db=db,
1558
+ embeddings=embeddings,
1559
+ memory_id=params.get("memory_id"),
1560
+ project_path=params.get("project_path"),
1561
+ limit=params.get("limit", 100)
1562
+ )
1124
1563
 
1125
- elif skill_id == "daily_log_append":
1126
- from services.daily_log import append_entry
1127
- return await append_entry(
1128
- project_path=params.get("project_path"),
1129
- content=params.get("content", query),
1130
- entry_type=params.get("entry_type", "note"),
1131
- session_id=session_id or params.get("session_id")
1132
- )
1133
1564
 
1134
- elif skill_id == "daily_log_append_session":
1135
- from services.daily_log import append_session_summary
1136
- return await append_session_summary(
1137
- project_path=params.get("project_path"),
1138
- session_id=session_id or params.get("session_id"),
1139
- decisions=params.get("decisions"),
1140
- accomplishments=params.get("accomplishments"),
1141
- notes=params.get("notes"),
1142
- errors_solved=params.get("errors_solved")
1143
- )
1565
+ async def _handle_curator_find_orphans(query, params, session_id):
1566
+ from skills.curator import curator_find_orphans as _curator_find_orphans
1567
+ return await _curator_find_orphans(
1568
+ db=db,
1569
+ embeddings=embeddings,
1570
+ project_path=params.get("project_path"),
1571
+ limit=params.get("limit", 50)
1572
+ )
1144
1573
 
1145
- elif skill_id == "daily_log_read":
1146
- from services.daily_log import load_recent_logs
1147
- return await load_recent_logs(
1148
- project_path=params.get("project_path"),
1149
- days=params.get("days", 2),
1150
- max_chars=params.get("max_chars", 8000)
1151
- )
1152
1574
 
1153
- elif skill_id == "daily_log_highlights":
1154
- from services.daily_log import get_today_highlights
1155
- return await get_today_highlights(
1156
- project_path=params.get("project_path"),
1157
- max_entries=params.get("max_entries", 10)
1158
- )
1575
+ # --- Memory Decay Skills ---
1159
1576
 
1160
- elif skill_id == "daily_log_list":
1161
- from services.daily_log import list_logs
1162
- return await list_logs(
1163
- project_path=params.get("project_path"),
1164
- limit=params.get("limit", 30)
1165
- )
1577
+ async def _handle_decay_maintenance(query, params, session_id):
1578
+ from services.memory_decay import MemoryDecayService
1579
+ from config import config
1580
+ decay_service = MemoryDecayService(
1581
+ db=db,
1582
+ archive_threshold=config.DECAY_ARCHIVE_THRESHOLD
1583
+ )
1584
+ return await decay_service.apply_decay()
1166
1585
 
1167
- elif skill_id == "sync_memory_md":
1168
- from services.memory_md_sync import sync_to_memory_md
1169
- return await sync_to_memory_md(
1170
- db=db,
1171
- project_path=params.get("project_path"),
1172
- min_importance=params.get("min_importance", 7),
1173
- min_pattern_success=params.get("min_pattern_success", 3)
1174
- )
1175
1586
 
1176
- elif skill_id == "read_memory_md":
1177
- from services.memory_md_sync import read_memory_md
1178
- return await read_memory_md(
1179
- project_path=params.get("project_path")
1180
- )
1587
+ async def _handle_decay_stats(query, params, session_id):
1588
+ from services.memory_decay import MemoryDecayService
1589
+ from config import config
1590
+ decay_service = MemoryDecayService(
1591
+ db=db,
1592
+ archive_threshold=config.DECAY_ARCHIVE_THRESHOLD
1593
+ )
1594
+ return await decay_service.get_decay_stats()
1181
1595
 
1182
- elif skill_id == "get_memory_md_summary":
1183
- from services.memory_md_sync import get_memory_md_summary
1184
- return await get_memory_md_summary(
1185
- project_path=params.get("project_path")
1186
- )
1187
1596
 
1188
- elif skill_id == "add_memory_md_fact":
1189
- from services.memory_md_sync import add_fact
1190
- return await add_fact(
1191
- project_path=params.get("project_path"),
1192
- fact=params.get("fact", query),
1193
- section=params.get("section", "anchors")
1194
- )
1597
+ async def _handle_decay_boost(query, params, session_id):
1598
+ from services.memory_decay import MemoryDecayService
1599
+ from config import config
1600
+ decay_service = MemoryDecayService(
1601
+ db=db,
1602
+ archive_threshold=config.DECAY_ARCHIVE_THRESHOLD
1603
+ )
1604
+ memory_id = params.get("memory_id")
1605
+ if not memory_id:
1606
+ return {"error": "memory_id is required"}
1607
+ return await decay_service.boost_on_access(int(memory_id))
1195
1608
 
1196
- elif skill_id == "check_flush_needed":
1197
- from services.compaction_flush import check_flush_needed
1198
- return await check_flush_needed(
1199
- db=db,
1200
- session_id=session_id or params.get("session_id"),
1201
- event_threshold=params.get("event_threshold", 50),
1202
- time_threshold_minutes=params.get("time_threshold_minutes", 30)
1203
- )
1204
1609
 
1205
- elif skill_id == "pre_compaction_flush":
1206
- from services.compaction_flush import execute_flush
1207
- return await execute_flush(
1208
- db=db,
1209
- project_path=params.get("project_path"),
1210
- session_id=session_id or params.get("session_id")
1211
- )
1610
+ # --- Tier 1 Auto-Generation Skill ---
1212
1611
 
1213
- elif skill_id == "list_flushes":
1214
- from services.compaction_flush import list_flushes
1215
- return await list_flushes(
1216
- project_path=params.get("project_path"),
1217
- limit=params.get("limit", 20)
1218
- )
1612
+ async def _handle_generate_tier1(query, params, session_id):
1613
+ from services.claude_md_sync import get_claude_md_sync
1614
+ sync_service = get_claude_md_sync(db, embeddings)
1615
+ return await sync_service.write_tier1_to_claude_md(
1616
+ project_path=params.get("project_path"),
1617
+ dry_run=params.get("dry_run", False)
1618
+ )
1219
1619
 
1220
- elif skill_id == "read_flush":
1221
- from services.compaction_flush import read_flush
1222
- return await read_flush(
1223
- project_path=params.get("project_path"),
1224
- filename=params.get("filename")
1225
- )
1226
1620
 
1227
- # ============================================================
1228
- # OUTCOME SPECTRUM SKILLS
1229
- # ============================================================
1621
+ # --- Session Review Skills ---
1230
1622
 
1231
- elif skill_id == "update_memory_outcome":
1232
- result = await db.update_memory_outcome(
1233
- memory_id=params.get("memory_id"),
1234
- outcome_status=params.get("outcome_status"),
1235
- fixed=params.get("fixed"),
1236
- did_not_fix=params.get("did_not_fix"),
1237
- caused=params.get("caused"),
1238
- superseded_by=params.get("superseded_by")
1239
- )
1240
- # Broadcast real-time update
1241
- if result.get("success"):
1242
- await broadcast_event(
1243
- EventTypes.MEMORY_UPDATED,
1244
- {
1245
- "memory_id": params.get("memory_id"),
1246
- "outcome_status": params.get("outcome_status"),
1247
- "action": "outcome_updated"
1248
- }
1249
- )
1250
- return result
1623
+ async def _handle_get_session_memories(query, params, session_id):
1624
+ return await get_session_memories(
1625
+ db=db,
1626
+ session_id=session_id or params.get("session_id"),
1627
+ include_patterns=params.get("include_patterns", False),
1628
+ limit=params.get("limit", 100)
1629
+ )
1251
1630
 
1252
- elif skill_id == "supersede_memory":
1253
- result = await db.supersede_memory(
1254
- old_memory_id=params.get("old_memory_id"),
1255
- new_memory_id=params.get("new_memory_id"),
1256
- reason=params.get("reason")
1257
- )
1258
- # Broadcast real-time update
1259
- if result.get("success"):
1260
- await broadcast_event(
1261
- EventTypes.MEMORY_UPDATED,
1262
- {
1263
- "old_memory_id": params.get("old_memory_id"),
1264
- "new_memory_id": params.get("new_memory_id"),
1265
- "action": "superseded"
1266
- }
1267
- )
1268
- return result
1269
1631
 
1270
- elif skill_id == "get_superseding_memory":
1271
- superseding = await db.get_superseding_memory(
1272
- memory_id=params.get("memory_id")
1273
- )
1274
- return {
1275
- "success": True,
1276
- "superseded": superseding is not None,
1277
- "superseding_memory": superseding
1278
- }
1632
+ async def _handle_review_session_memories(query, params, session_id):
1633
+ return await review_session_memories(
1634
+ db=db,
1635
+ session_id=session_id or params.get("session_id"),
1636
+ reviews=params.get("reviews", [])
1637
+ )
1279
1638
 
1280
- # ============================================================
1281
- # CURATOR AGENT SKILLS
1282
- # ============================================================
1283
1639
 
1284
- elif skill_id == "curator_explore":
1285
- from skills.curator import curator_explore
1286
- return await curator_explore(
1287
- db=db,
1288
- embeddings=embeddings,
1289
- start_node_id=params.get("start_node_id"),
1290
- max_depth=params.get("max_depth", 3),
1291
- mode=params.get("mode", "bfs"),
1292
- relationship_filter=params.get("relationship_filter")
1293
- )
1640
+ async def _handle_suggest_session_reviews(query, params, session_id):
1641
+ return await suggest_session_reviews(
1642
+ db=db,
1643
+ embeddings=embeddings,
1644
+ session_id=session_id or params.get("session_id")
1645
+ )
1294
1646
 
1295
- elif skill_id == "curator_find_duplicates":
1296
- from skills.curator import curator_find_duplicates
1297
- return await curator_find_duplicates(
1298
- db=db,
1299
- embeddings=embeddings,
1300
- project_path=params.get("project_path"),
1301
- similarity_threshold=params.get("similarity_threshold", 0.92),
1302
- limit=params.get("limit", 50)
1303
- )
1304
1647
 
1305
- elif skill_id == "curator_suggest_links":
1306
- from skills.curator import curator_suggest_links
1307
- return await curator_suggest_links(
1308
- db=db,
1309
- embeddings=embeddings,
1310
- memory_id=params.get("memory_id"),
1311
- project_path=params.get("project_path"),
1312
- similarity_threshold=params.get("similarity_threshold", 0.7),
1313
- limit=params.get("limit", 20)
1314
- )
1648
+ async def _handle_get_recent_sessions(query, params, session_id):
1649
+ return await get_recent_sessions(
1650
+ db=db,
1651
+ project_path=params.get("project_path"),
1652
+ limit=params.get("limit", 10)
1653
+ )
1315
1654
 
1316
- elif skill_id == "curator_merge":
1317
- from skills.curator import curator_merge
1318
- return await curator_merge(
1319
- db=db,
1320
- embeddings=embeddings,
1321
- keep_id=params.get("keep_id"),
1322
- remove_ids=params.get("remove_ids", []),
1323
- merge_content=params.get("merge_content", False)
1324
- )
1325
1655
 
1326
- elif skill_id == "curator_get_summary":
1327
- from skills.curator import curator_get_summary
1328
- return await curator_get_summary(
1329
- db=db,
1330
- embeddings=embeddings,
1331
- query=params.get("query", query),
1332
- project_path=params.get("project_path"),
1333
- max_memories=params.get("max_memories", 10),
1334
- include_graph=params.get("include_graph", True)
1335
- )
1656
+ async def _handle_bulk_review_by_type(query, params, session_id):
1657
+ return await bulk_review_by_type(
1658
+ db=db,
1659
+ session_id=session_id or params.get("session_id"),
1660
+ type_decisions=params.get("type_decisions", {})
1661
+ )
1336
1662
 
1337
- elif skill_id == "curator_run_maintenance":
1338
- from skills.curator import curator_run_maintenance
1339
- return await curator_run_maintenance(
1340
- db=db,
1341
- embeddings=embeddings,
1342
- project_path=params.get("project_path"),
1343
- tasks=params.get("tasks")
1344
- )
1345
1663
 
1346
- elif skill_id == "curator_get_report":
1347
- from skills.curator import curator_get_report
1348
- return await curator_get_report(
1349
- db=db,
1350
- embeddings=embeddings,
1351
- project_path=params.get("project_path")
1352
- )
1664
+ # ============================================================
1665
+ # SKILL DISPATCH TABLE
1666
+ # ============================================================
1667
+ # Maps skill_id strings to their async handler functions.
1668
+ # All handlers share the signature: (query, params, session_id) -> dict
1669
+ # ============================================================
1353
1670
 
1354
- elif skill_id == "curator_get_status":
1355
- from skills.curator import curator_get_status
1356
- return await curator_get_status(
1357
- db=db,
1358
- embeddings=embeddings
1359
- )
1671
+ SKILL_DISPATCH = {
1672
+ # Core Memory
1673
+ "store_memory": _handle_store_memory,
1674
+ "store_project": _handle_store_project,
1675
+ "store_pattern": _handle_store_pattern,
1676
+ "retrieve_memory": _handle_retrieve_memory,
1677
+ "semantic_search": _handle_semantic_search,
1678
+ "search_patterns": _handle_search_patterns,
1679
+ "get_project_context": _handle_get_project_context,
1680
+
1681
+ # Session
1682
+ "summarize_session": _handle_summarize_session,
1683
+ "auto_summarize_session": _handle_auto_summarize_session,
1684
+ "get_session_handoff": _handle_get_session_handoff,
1685
+ "create_diary_entry": _handle_create_diary_entry,
1686
+ "check_session_inactivity": _handle_check_session_inactivity,
1687
+ "get_stats": _handle_get_stats,
1688
+
1689
+ # Timeline
1690
+ "timeline_log": _handle_timeline_log,
1691
+ "timeline_log_batch": _handle_timeline_log_batch,
1692
+ "timeline_get": _handle_timeline_get,
1693
+ "timeline_search": _handle_timeline_search,
1694
+ "timeline_auto_detect": _handle_timeline_auto_detect,
1695
+ "timeline_chain": _handle_timeline_chain,
1696
+
1697
+ # State
1698
+ "state_get": _handle_state_get,
1699
+ "state_update": _handle_state_update,
1700
+ "state_init_session": _handle_state_init_session,
1701
+
1702
+ # Checkpoint
1703
+ "checkpoint_create": _handle_checkpoint_create,
1704
+ "checkpoint_load": _handle_checkpoint_load,
1705
+ "checkpoint_list": _handle_checkpoint_list,
1706
+
1707
+ # Grounding (Anti-Hallucination)
1708
+ "context_refresh": _handle_context_refresh,
1709
+ "check_contradictions": _handle_check_contradictions,
1710
+ "verify_entity": _handle_verify_entity,
1711
+ "mark_anchor": _handle_mark_anchor,
1712
+ "get_unresolved_conflicts": _handle_get_unresolved_conflicts,
1713
+ "resolve_conflict": _handle_resolve_conflict,
1714
+ "get_anchor_history": _handle_get_anchor_history,
1715
+ "auto_resolve_conflicts": _handle_auto_resolve_conflicts,
1716
+
1717
+ # Self-Correcting Confidence
1718
+ "memory_worked": _handle_memory_worked,
1719
+ "memory_failed": _handle_memory_failed,
1720
+ "get_reliability_stats": _handle_get_reliability_stats,
1721
+ "get_unreliable_memories": _handle_get_unreliable_memories,
1722
+ "reset_memory_reliability": _handle_reset_memory_reliability,
1723
+
1724
+ # CLAUDE.MD Management
1725
+ "claude_md_read": _handle_claude_md_read,
1726
+ "claude_md_add_section": _handle_claude_md_add_section,
1727
+ "claude_md_update_section": _handle_claude_md_update_section,
1728
+ "claude_md_add_instruction": _handle_claude_md_add_instruction,
1729
+ "claude_md_list_sections": _handle_claude_md_list_sections,
1730
+ "claude_md_suggest": _handle_claude_md_suggest,
1731
+
1732
+ # Verification
1733
+ "best_of_n_verify": _handle_best_of_n_verify,
1734
+ "extract_quotes": _handle_extract_quotes,
1735
+ "require_grounding": _handle_require_grounding,
1736
+
1737
+ # Cross-Session Learning
1738
+ "run_aggregation": _handle_run_aggregation,
1739
+ "get_insights": _handle_get_insights,
1740
+ "suggest_improvements": _handle_suggest_improvements,
1741
+ "record_insight_feedback": _handle_record_insight_feedback,
1742
+ "mark_insight_applied": _handle_mark_insight_applied,
1743
+ "get_project_insights": _handle_get_project_insights,
1744
+
1745
+ # Memory Cleanup
1746
+ "memory_cleanup": _handle_memory_cleanup,
1747
+ "get_archived_memories": _handle_get_archived_memories,
1748
+ "restore_memory": _handle_restore_memory,
1749
+ "get_cleanup_config": _handle_get_cleanup_config,
1750
+ "set_cleanup_config": _handle_set_cleanup_config,
1751
+ "get_cleanup_stats": _handle_get_cleanup_stats,
1752
+ "purge_expired_archives": _handle_purge_expired_archives,
1753
+
1754
+ # Admin (Embedding Model Management)
1755
+ "get_embedding_status": _handle_get_embedding_status,
1756
+ "switch_embedding_model": _handle_switch_embedding_model,
1757
+ "reindex_memories": _handle_reindex_memories,
1758
+ "get_reindex_progress": _handle_get_reindex_progress,
1759
+ "cancel_reindex": _handle_cancel_reindex,
1760
+ "get_model_info": _handle_get_model_info,
1761
+ "get_system_stats": _handle_get_system_stats,
1762
+
1763
+ # MoltBot-Inspired (Human-Readable Transparency)
1764
+ "daily_log_append": _handle_daily_log_append,
1765
+ "daily_log_append_session": _handle_daily_log_append_session,
1766
+ "daily_log_read": _handle_daily_log_read,
1767
+ "daily_log_highlights": _handle_daily_log_highlights,
1768
+ "daily_log_list": _handle_daily_log_list,
1769
+ "sync_memory_md": _handle_sync_memory_md,
1770
+ "read_memory_md": _handle_read_memory_md,
1771
+ "get_memory_md_summary": _handle_get_memory_md_summary,
1772
+ "add_memory_md_fact": _handle_add_memory_md_fact,
1773
+ "check_flush_needed": _handle_check_flush_needed,
1774
+ "pre_compaction_flush": _handle_pre_compaction_flush,
1775
+ "list_flushes": _handle_list_flushes,
1776
+ "read_flush": _handle_read_flush,
1777
+
1778
+ # Outcome Spectrum
1779
+ "update_memory_outcome": _handle_update_memory_outcome,
1780
+ "supersede_memory": _handle_supersede_memory,
1781
+ "get_superseding_memory": _handle_get_superseding_memory,
1782
+
1783
+ # Curator Agent
1784
+ "curator_explore": _handle_curator_explore,
1785
+ "curator_find_duplicates": _handle_curator_find_duplicates,
1786
+ "curator_suggest_links": _handle_curator_suggest_links,
1787
+ "curator_merge": _handle_curator_merge,
1788
+ "curator_get_summary": _handle_curator_get_summary,
1789
+ "curator_run_maintenance": _handle_curator_run_maintenance,
1790
+ "curator_get_report": _handle_curator_get_report,
1791
+ "curator_get_status": _handle_curator_get_status,
1792
+ "curator_score_quality": _handle_curator_score_quality,
1793
+ "curator_find_orphans": _handle_curator_find_orphans,
1794
+
1795
+ # Memory Decay
1796
+ "decay_maintenance": _handle_decay_maintenance,
1797
+ "decay_stats": _handle_decay_stats,
1798
+ "decay_boost": _handle_decay_boost,
1799
+
1800
+ # Tier 1 Auto-Generation
1801
+ "generate_tier1": _handle_generate_tier1,
1802
+
1803
+ # Session Review
1804
+ "get_session_memories": _handle_get_session_memories,
1805
+ "review_session_memories": _handle_review_session_memories,
1806
+ "suggest_session_reviews": _handle_suggest_session_reviews,
1807
+ "get_recent_sessions": _handle_get_recent_sessions,
1808
+ "bulk_review_by_type": _handle_bulk_review_by_type,
1809
+ }
1360
1810
 
1361
- elif skill_id == "curator_score_quality":
1362
- from skills.curator import curator_score_quality
1363
- return await curator_score_quality(
1364
- db=db,
1365
- embeddings=embeddings,
1366
- memory_id=params.get("memory_id"),
1367
- project_path=params.get("project_path"),
1368
- limit=params.get("limit", 100)
1369
- )
1370
1811
 
1371
- elif skill_id == "curator_find_orphans":
1372
- from skills.curator import curator_find_orphans
1373
- return await curator_find_orphans(
1374
- db=db,
1375
- embeddings=embeddings,
1376
- project_path=params.get("project_path"),
1377
- limit=params.get("limit", 50)
1378
- )
1812
+ async def execute_skill(
1813
+ skill_id: str,
1814
+ query: str,
1815
+ params: Dict[str, Any],
1816
+ session_id: Optional[str] = None
1817
+ ) -> Dict[str, Any]:
1818
+ """Execute the specified skill with enhanced context support.
1379
1819
 
1380
- else:
1820
+ Dispatches to the appropriate handler via SKILL_DISPATCH lookup table.
1821
+ Each handler receives (query, params, session_id) and returns a dict.
1822
+ """
1823
+ handler = SKILL_DISPATCH.get(skill_id)
1824
+ if handler is None:
1381
1825
  raise ValueError(f"Unknown skill: {skill_id}")
1826
+ return await handler(query, params, session_id)
1382
1827
 
1383
1828
 
1384
1829
  # ============= REST API Endpoints =============
@@ -1416,6 +1861,7 @@ async def api_get_stats():
1416
1861
  stats["timeline_stats_error"] = f"Unexpected error: {str(e)}"
1417
1862
 
1418
1863
  stats["success"] = True
1864
+ stats["operation_metrics"] = metrics.to_dict()
1419
1865
  return stats
1420
1866
 
1421
1867
 
@@ -1538,7 +1984,12 @@ async def api_get_timeline(
1538
1984
  ):
1539
1985
  """Get timeline events with optional filtering."""
1540
1986
  try:
1541
- query = "SELECT * FROM timeline_events WHERE 1=1"
1987
+ # Exclude the embedding column - it's a large binary blob that makes responses huge
1988
+ query = """SELECT id, session_id, project_path, event_type, sequence_num,
1989
+ summary, details, parent_event_id, root_event_id, entities,
1990
+ status, outcome, confidence, is_anchor, is_reversible,
1991
+ needs_verification, created_at
1992
+ FROM timeline_events WHERE 1=1"""
1542
1993
  params = []
1543
1994
 
1544
1995
  if project_path:
@@ -2168,6 +2619,57 @@ async def liveness_check():
2168
2619
  return {"alive": True, "timestamp": datetime.now().isoformat()}
2169
2620
 
2170
2621
 
2622
+ @app.post("/health/retry")
2623
+ async def retry_connection():
2624
+ """Force retry connection to Ollama.
2625
+
2626
+ Clears health check cache and attempts to reconnect.
2627
+ Used by dashboard to manually trigger recovery.
2628
+ """
2629
+ # Force health check with fresh attempt
2630
+ health = await embeddings.check_health(force=True)
2631
+
2632
+ # If still unhealthy, try to ping Ollama directly
2633
+ if not health.get("healthy"):
2634
+ try:
2635
+ import subprocess
2636
+ import platform
2637
+
2638
+ # Try to check if Ollama is running
2639
+ if platform.system() == "Windows":
2640
+ # On Windows, try to start Ollama if not running
2641
+ result = subprocess.run(
2642
+ ["tasklist", "/FI", "IMAGENAME eq ollama.exe"],
2643
+ capture_output=True,
2644
+ text=True,
2645
+ timeout=5
2646
+ )
2647
+ ollama_running = "ollama.exe" in result.stdout
2648
+
2649
+ if not ollama_running:
2650
+ # Try to start Ollama
2651
+ subprocess.Popen(
2652
+ ["ollama", "serve"],
2653
+ creationflags=subprocess.CREATE_NO_WINDOW,
2654
+ stdout=subprocess.DEVNULL,
2655
+ stderr=subprocess.DEVNULL
2656
+ )
2657
+ # Wait a moment for startup
2658
+ import asyncio
2659
+ await asyncio.sleep(2)
2660
+ # Retry health check
2661
+ health = await embeddings.check_health(force=True)
2662
+ except Exception as e:
2663
+ logger.warning(f"Could not auto-start Ollama: {e}")
2664
+
2665
+ return {
2666
+ "success": health.get("healthy", False),
2667
+ "health": health,
2668
+ "message": "Healthy" if health.get("healthy") else "Still degraded - check if Ollama is running",
2669
+ "timestamp": datetime.now().isoformat()
2670
+ }
2671
+
2672
+
2171
2673
  @app.get("/api/index-stats")
2172
2674
  async def get_index_stats():
2173
2675
  """Get FAISS vector index statistics.
@@ -3581,21 +4083,33 @@ async def get_all_agents():
3581
4083
 
3582
4084
  @app.get("/api/mcps")
3583
4085
  async def get_all_mcps():
3584
- """Get all available MCP servers."""
4086
+ """Get all available MCP servers with live configured status."""
4087
+ mcps = load_configured_mcps()
4088
+ configured_count = sum(1 for m in mcps if m.get("configured"))
3585
4089
  return {
3586
4090
  "success": True,
3587
- "mcps": AVAILABLE_MCPS,
3588
- "total": len(AVAILABLE_MCPS)
4091
+ "mcps": mcps,
4092
+ "total": len(mcps),
4093
+ "configured": configured_count
3589
4094
  }
3590
4095
 
3591
4096
 
3592
4097
  @app.get("/api/hooks")
3593
4098
  async def get_all_hooks():
3594
- """Get all available hooks."""
4099
+ """Get all available hooks with live configured status."""
4100
+ hooks = load_configured_hooks()
4101
+ configured_count = sum(1 for h in hooks if h.get("configured"))
4102
+ # Group by trigger for dashboard convenience
4103
+ by_trigger: dict = {}
4104
+ for h in hooks:
4105
+ trigger = h.get("trigger", "Unknown")
4106
+ by_trigger.setdefault(trigger, []).append(h)
3595
4107
  return {
3596
4108
  "success": True,
3597
- "hooks": AVAILABLE_HOOKS,
3598
- "total": len(AVAILABLE_HOOKS)
4109
+ "hooks": hooks,
4110
+ "total": len(hooks),
4111
+ "configured": configured_count,
4112
+ "by_trigger": by_trigger
3599
4113
  }
3600
4114
 
3601
4115
 
@@ -3647,7 +4161,8 @@ async def get_project_config(project_path: str):
3647
4161
  'settings': {}
3648
4162
  }
3649
4163
 
3650
- # Build MCP status map
4164
+ # Build MCP status map using dynamic loader
4165
+ live_mcps = load_configured_mcps(project_path)
3651
4166
  mcp_status = {}
3652
4167
  for config in (mcp_configs or []):
3653
4168
  mcp_status[config['mcp_id']] = {
@@ -3655,14 +4170,18 @@ async def get_project_config(project_path: str):
3655
4170
  'settings': json.loads(config['settings']) if config['settings'] else {}
3656
4171
  }
3657
4172
 
3658
- for mcp in AVAILABLE_MCPS:
4173
+ for mcp in live_mcps:
3659
4174
  if mcp['id'] not in mcp_status:
3660
4175
  mcp_status[mcp['id']] = {
3661
4176
  'enabled': mcp['default_enabled'],
4177
+ 'configured': mcp.get('configured', False),
3662
4178
  'settings': {}
3663
4179
  }
4180
+ else:
4181
+ mcp_status[mcp['id']]['configured'] = mcp.get('configured', False)
3664
4182
 
3665
- # Build hook status map
4183
+ # Build hook status map using dynamic loader
4184
+ live_hooks = load_configured_hooks(project_path)
3666
4185
  hook_status = {}
3667
4186
  for config in (hook_configs or []):
3668
4187
  hook_status[config['hook_id']] = {
@@ -3670,12 +4189,17 @@ async def get_project_config(project_path: str):
3670
4189
  'settings': json.loads(config['settings']) if config['settings'] else {}
3671
4190
  }
3672
4191
 
3673
- for hook in AVAILABLE_HOOKS:
4192
+ for hook in live_hooks:
3674
4193
  if hook['id'] not in hook_status:
3675
4194
  hook_status[hook['id']] = {
3676
4195
  'enabled': hook['default_enabled'],
4196
+ 'configured': hook.get('configured', False),
4197
+ 'trigger': hook.get('trigger', ''),
3677
4198
  'settings': {}
3678
4199
  }
4200
+ else:
4201
+ hook_status[hook['id']]['configured'] = hook.get('configured', False)
4202
+ hook_status[hook['id']]['trigger'] = hook.get('trigger', '')
3679
4203
 
3680
4204
  return {
3681
4205
  "success": True,
@@ -3687,10 +4211,12 @@ async def get_project_config(project_path: str):
3687
4211
  "stats": {
3688
4212
  "enabled_agents": sum(1 for a in agent_status.values() if a['enabled']),
3689
4213
  "total_agents": len(AVAILABLE_AGENTS),
3690
- "enabled_mcps": sum(1 for m in mcp_status.values() if m['enabled']),
3691
- "total_mcps": len(AVAILABLE_MCPS),
3692
- "enabled_hooks": sum(1 for h in hook_status.values() if h['enabled']),
3693
- "total_hooks": len(AVAILABLE_HOOKS)
4214
+ "enabled_mcps": sum(1 for m in mcp_status.values() if m.get('enabled')),
4215
+ "total_mcps": len(live_mcps),
4216
+ "configured_mcps": sum(1 for m in mcp_status.values() if m.get('configured')),
4217
+ "enabled_hooks": sum(1 for h in hook_status.values() if h.get('enabled')),
4218
+ "total_hooks": len(live_hooks),
4219
+ "configured_hooks": sum(1 for h in hook_status.values() if h.get('configured'))
3694
4220
  }
3695
4221
  }
3696
4222
  except DatabaseError as e:
@@ -4029,7 +4555,7 @@ async def get_graph_node(memory_id: int):
4029
4555
  """
4030
4556
  try:
4031
4557
  # Get the memory itself
4032
- memory = await db.get_memory_by_id(memory_id)
4558
+ memory = await db.get_memory(memory_id)
4033
4559
  if not memory:
4034
4560
  return {"success": False, "error": "Memory not found"}
4035
4561
 
@@ -4486,6 +5012,224 @@ async def curator_config_update_endpoint(
4486
5012
  return {"success": False, "error": str(e)}
4487
5013
 
4488
5014
 
5015
+ # ============= Memory Decay API Endpoints =============
5016
+
5017
+ @app.post("/api/decay/run")
5018
+ async def decay_run_endpoint():
5019
+ """Run memory decay maintenance - evaluate all decayable memories and archive expired ones."""
5020
+ try:
5021
+ from services.memory_decay import MemoryDecayService
5022
+ from config import config as app_config
5023
+ decay_service = MemoryDecayService(
5024
+ db=db,
5025
+ archive_threshold=app_config.DECAY_ARCHIVE_THRESHOLD
5026
+ )
5027
+ result = await decay_service.apply_decay()
5028
+ return result
5029
+ except Exception as e:
5030
+ logger.error(f"Decay maintenance failed: {e}")
5031
+ return {"success": False, "error": str(e)}
5032
+
5033
+
5034
+ @app.get("/api/decay/stats")
5035
+ async def decay_stats_endpoint():
5036
+ """Get memory decay statistics - permanent vs decayable counts, at-risk memories."""
5037
+ try:
5038
+ from services.memory_decay import MemoryDecayService
5039
+ from config import config as app_config
5040
+ decay_service = MemoryDecayService(
5041
+ db=db,
5042
+ archive_threshold=app_config.DECAY_ARCHIVE_THRESHOLD
5043
+ )
5044
+ result = await decay_service.get_decay_stats()
5045
+ return result
5046
+ except Exception as e:
5047
+ logger.error(f"Decay stats failed: {e}")
5048
+ return {"success": False, "error": str(e)}
5049
+
5050
+
5051
+ @app.post("/api/decay/boost/{memory_id}")
5052
+ async def decay_boost_endpoint(memory_id: int):
5053
+ """Boost a memory's access count to resist decay."""
5054
+ try:
5055
+ from services.memory_decay import MemoryDecayService
5056
+ from config import config as app_config
5057
+ decay_service = MemoryDecayService(
5058
+ db=db,
5059
+ archive_threshold=app_config.DECAY_ARCHIVE_THRESHOLD
5060
+ )
5061
+ result = await decay_service.boost_on_access(memory_id)
5062
+ return result
5063
+ except Exception as e:
5064
+ logger.error(f"Decay boost failed: {e}")
5065
+ return {"success": False, "error": str(e)}
5066
+
5067
+
5068
+ # ============= Tier 1 Auto-Generation API Endpoint =============
5069
+
5070
+ @app.post("/api/tier1/generate")
5071
+ async def tier1_generate_endpoint(request: Request):
5072
+ """Generate Tier 1 context from top memories and write to CLAUDE.md.
5073
+
5074
+ Auto-generates a ranked summary of the most important memories and
5075
+ writes it into CLAUDE.md between marker comments. All manually-written
5076
+ content in CLAUDE.md is preserved.
5077
+
5078
+ Optional JSON body:
5079
+ project_path: Filter to a specific project
5080
+ dry_run: If true, return preview without writing (default false)
5081
+ """
5082
+ try:
5083
+ body = {}
5084
+ try:
5085
+ body = await request.json()
5086
+ except Exception:
5087
+ pass # No body is fine, all params are optional
5088
+
5089
+ from services.claude_md_sync import get_claude_md_sync
5090
+ sync_service = get_claude_md_sync(db, embeddings)
5091
+ result = await sync_service.write_tier1_to_claude_md(
5092
+ project_path=body.get("project_path"),
5093
+ dry_run=body.get("dry_run", False)
5094
+ )
5095
+ return result
5096
+ except Exception as e:
5097
+ logger.error(f"Tier 1 generation failed: {e}")
5098
+ return {"success": False, "error": str(e)}
5099
+
5100
+
5101
+ # ============= CLaRa-Inspired Memory Enhancement Endpoints =============
5102
+
5103
+ @app.get("/api/tiers/stats")
5104
+ async def tier_stats_endpoint():
5105
+ """Get memory distribution across tiers (hot/warm/cold)."""
5106
+ try:
5107
+ from services.tier_manager import TierManager
5108
+ tier_mgr = TierManager(db)
5109
+ return await tier_mgr.get_tier_stats()
5110
+ except Exception as e:
5111
+ logger.error(f"Tier stats failed: {e}")
5112
+ return {"error": str(e)}
5113
+
5114
+
5115
+ @app.post("/api/tiers/maintenance")
5116
+ async def tier_maintenance_endpoint(request: Request):
5117
+ """Run tier maintenance (evaluate and update all memory tiers)."""
5118
+ try:
5119
+ body = {}
5120
+ try:
5121
+ body = await request.json()
5122
+ except Exception:
5123
+ pass
5124
+ from services.tier_manager import TierManager
5125
+ tier_mgr = TierManager(db)
5126
+ return await tier_mgr.run_tier_maintenance(
5127
+ skip_recent_hours=body.get("skip_recent_hours", 24)
5128
+ )
5129
+ except Exception as e:
5130
+ logger.error(f"Tier maintenance failed: {e}")
5131
+ return {"error": str(e)}
5132
+
5133
+
5134
+ @app.post("/api/consolidation/run")
5135
+ async def consolidation_run_endpoint():
5136
+ """Manually trigger memory consolidation."""
5137
+ try:
5138
+ from services.consolidation import ConsolidationService
5139
+ consolidator = ConsolidationService(db, embeddings)
5140
+ return await consolidator.run_consolidation()
5141
+ except Exception as e:
5142
+ logger.error(f"Consolidation failed: {e}")
5143
+ return {"error": str(e)}
5144
+
5145
+
5146
+ @app.get("/api/consolidation/candidates")
5147
+ async def consolidation_candidates_endpoint():
5148
+ """Preview consolidation candidates without executing."""
5149
+ try:
5150
+ from services.consolidation import ConsolidationService
5151
+ consolidator = ConsolidationService(db, embeddings)
5152
+ groups = await consolidator.find_consolidation_candidates()
5153
+ return {
5154
+ "candidate_groups": len(groups),
5155
+ "groups": [
5156
+ {
5157
+ "size": len(g),
5158
+ "types": list(set(m.get('type', 'chunk') for m in g)),
5159
+ "ids": [m['id'] for m in g],
5160
+ "preview": g[0]['content'][:100] if g else ''
5161
+ }
5162
+ for g in groups
5163
+ ]
5164
+ }
5165
+ except Exception as e:
5166
+ logger.error(f"Consolidation candidates failed: {e}")
5167
+ return {"error": str(e)}
5168
+
5169
+
5170
+ @app.post("/api/consolidation/{consolidated_id}/restore")
5171
+ async def consolidation_restore_endpoint(consolidated_id: int):
5172
+ """Deconsolidate: restore original memories from a consolidated memory."""
5173
+ try:
5174
+ from services.consolidation import ConsolidationService
5175
+ consolidator = ConsolidationService(db, embeddings)
5176
+ return await consolidator.deconsolidate(consolidated_id)
5177
+ except Exception as e:
5178
+ logger.error(f"Deconsolidation failed: {e}")
5179
+ return {"error": str(e)}
5180
+
5181
+
5182
+ @app.get("/api/consolidation/stats")
5183
+ async def consolidation_stats_endpoint():
5184
+ """Get consolidation statistics."""
5185
+ try:
5186
+ from services.consolidation import ConsolidationService
5187
+ consolidator = ConsolidationService(db, embeddings)
5188
+ return await consolidator.get_consolidation_stats()
5189
+ except Exception as e:
5190
+ logger.error(f"Consolidation stats failed: {e}")
5191
+ return {"error": str(e)}
5192
+
5193
+
5194
+ @app.get("/api/embedding-pipeline/stats")
5195
+ async def embedding_pipeline_stats_endpoint():
5196
+ """Get embedding pipeline statistics (cache hits, batch stats)."""
5197
+ try:
5198
+ from services.embedding_pipeline import get_embedding_pipeline
5199
+ pipeline = get_embedding_pipeline()
5200
+ if pipeline:
5201
+ return pipeline.get_stats()
5202
+ return {"error": "Pipeline not initialized"}
5203
+ except Exception as e:
5204
+ logger.error(f"Embedding pipeline stats failed: {e}")
5205
+ return {"error": str(e)}
5206
+
5207
+
5208
+ @app.post("/api/embedding-pipeline/precompute")
5209
+ async def embedding_precompute_endpoint():
5210
+ """Manually trigger embedding pre-computation for memories with missing embeddings."""
5211
+ try:
5212
+ from services.embedding_pipeline import get_embedding_pipeline
5213
+ pipeline = get_embedding_pipeline()
5214
+ if pipeline:
5215
+ return await pipeline.precompute_missing_embeddings()
5216
+ return {"error": "Pipeline not initialized"}
5217
+ except Exception as e:
5218
+ logger.error(f"Embedding precompute failed: {e}")
5219
+ return {"error": str(e)}
5220
+
5221
+
5222
+ @app.post("/api/embeddings/migrate-binary")
5223
+ async def embeddings_migrate_binary_endpoint():
5224
+ """Migrate existing JSON embeddings to binary format for storage savings."""
5225
+ try:
5226
+ result = await db.migrate_embeddings_to_binary()
5227
+ return {"success": True, **result}
5228
+ except Exception as e:
5229
+ logger.error(f"Embedding migration failed: {e}")
5230
+ return {"error": str(e)}
5231
+
5232
+
4489
5233
  if __name__ == "__main__":
4490
5234
  import uvicorn
4491
5235
  uvicorn.run(