nexo-brain 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/nexo-brain.js +92 -9
- package/bin/postinstall.js +22 -15
- package/package.json +7 -4
- package/src/auto_update.py +194 -5
- package/src/crons/sync.py +6 -2
- package/src/db/_core.py +1 -0
- package/src/db/_entities.py +1 -0
- package/src/db/_episodic.py +1 -0
- package/src/db/_learnings.py +1 -0
- package/src/db/_reminders.py +1 -0
- package/src/db/_schema.py +11 -1
- package/src/db/_sessions.py +1 -0
- package/src/db/_skills.py +1 -0
- package/src/hooks/capture-tool-logs.sh +23 -6
- package/src/hooks/session-start.sh +4 -3
- package/src/plugin_loader.py +1 -0
- package/src/plugins/update.py +377 -26
- package/src/scripts/deep-sleep/apply_findings.py +1 -0
- package/src/scripts/deep-sleep/collect.py +1 -0
- package/src/scripts/deep-sleep/extract.py +1 -0
- package/src/scripts/deep-sleep/synthesize.py +1 -0
- package/src/scripts/nexo-catchup.py +29 -4
- package/src/scripts/nexo-daily-self-audit.py +21 -1
- package/src/scripts/nexo-evolution-run.py +21 -1
- package/src/scripts/nexo-learning-housekeep.py +1 -0
- package/src/scripts/nexo-postmortem-consolidator.py +34 -9
- package/src/scripts/nexo-sleep.py +32 -10
- package/src/scripts/nexo-synthesis.py +29 -9
- package/src/scripts/nexo-update.sh +109 -7
- package/src/scripts/nexo-watchdog.sh +122 -58
- package/src/server.py +66 -1
- package/src/tools_coordination.py +1 -0
- package/src/tools_sessions.py +1 -0
- package/scripts/migrate-to-unified 2.sh +0 -813
- package/scripts/migrate-to-unified.sh +0 -813
- package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
- package/scripts/migrate-v1.5-to-v1.6.py +0 -778
- package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
- package/scripts/migrate-v1.7-to-v1.8.py +0 -214
- package/scripts/nexo-preflight.sh +0 -236
- package/scripts/pre-commit-check 2.sh +0 -55
- package/scripts/pre-commit-check.sh +0 -55
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/auto_close_sessions 2.py +0 -159
- package/src/auto_update 2.py +0 -634
- package/src/claim_graph 2.py +0 -323
- package/src/cognitive/__init__ 2.py +0 -62
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/cognitive/_core 2.py +0 -567
- package/src/cognitive/_decay 2.py +0 -382
- package/src/cognitive/_ingest 2.py +0 -892
- package/src/cognitive/_memory 2.py +0 -912
- package/src/cognitive/_search 2.py +0 -949
- package/src/cognitive/_trust 2.py +0 -464
- package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
- package/src/crons/manifest 2.json +0 -106
- package/src/crons/sync 2.py +0 -217
- package/src/dashboard/__init__ 2.py +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app 2.py +0 -789
- package/src/db/__init__ 2.py +0 -89
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core 2.py +0 -417
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_entities 2.py +0 -178
- package/src/db/_episodic 2.py +0 -738
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_learnings 2.py +0 -168
- package/src/db/_reminders 2.py +0 -338
- package/src/db/_schema 2.py +0 -364
- package/src/db/_sessions 2.py +0 -300
- package/src/db/_tasks 2.py +0 -91
- package/src/evolution_cycle 2.py +0 -266
- package/src/hnsw_index 2.py +0 -254
- package/src/hooks/auto_capture 2.py +0 -208
- package/src/hooks/caffeinate-guard 2.sh +0 -8
- package/src/hooks/capture-session 2.sh +0 -21
- package/src/hooks/capture-tool-logs 2.sh +0 -127
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -148
- package/src/hooks/pre-compact 2.sh +0 -151
- package/src/hooks/session-start 2.sh +0 -268
- package/src/hooks/session-stop 2.sh +0 -140
- package/src/kg_populate 2.py +0 -290
- package/src/knowledge_graph 2.py +0 -257
- package/src/maintenance 2.py +0 -59
- package/src/migrate_embeddings 2.py +0 -122
- package/src/plugin_loader 2.py +0 -202
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/adaptive_mode 2.py +0 -805
- package/src/plugins/agents 2.py +0 -52
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -104
- package/src/plugins/cognitive_memory 2.py +0 -564
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -299
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -533
- package/src/plugins/evolution 2.py +0 -115
- package/src/plugins/guard 2.py +0 -746
- package/src/plugins/knowledge_graph_tools 2.py +0 -105
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/update 2.py +0 -256
- package/src/requirements 2.txt +0 -12
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +0 -331
- package/src/rules/migrate 2.py +0 -207
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/check-context 2.py +0 -264
- package/src/scripts/nexo-auto-update 2.py +0 -6
- package/src/scripts/nexo-backup 2.sh +0 -25
- package/src/scripts/nexo-brain-activation 2.sh +0 -140
- package/src/scripts/nexo-catchup 2.py +0 -242
- package/src/scripts/nexo-cognitive-decay 2.py +0 -182
- package/src/scripts/nexo-daily-self-audit 2.py +0 -552
- package/src/scripts/nexo-deep-sleep 2.sh +0 -97
- package/src/scripts/nexo-evolution-run 2.py +0 -597
- package/src/scripts/nexo-followup-hygiene 2.py +0 -112
- package/src/scripts/nexo-github-monitor 2.py +0 -256
- package/src/scripts/nexo-immune 2.py +0 -927
- package/src/scripts/nexo-inbox-hook 2.sh +0 -74
- package/src/scripts/nexo-install 2.py +0 -6
- package/src/scripts/nexo-learning-housekeep 2.py +0 -245
- package/src/scripts/nexo-learning-validator 2.py +0 -207
- package/src/scripts/nexo-migrate 2.py +0 -232
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
- package/src/scripts/nexo-reflection 2.py +0 -253
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-send-email 2.py +0 -25
- package/src/scripts/nexo-send-email.py +0 -25
- package/src/scripts/nexo-send-reply 2.py +0 -178
- package/src/scripts/nexo-send-reply.py +0 -178
- package/src/scripts/nexo-sleep 2.py +0 -592
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-synthesis 2.py +0 -253
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -161
- package/src/scripts/nexo-watchdog 2.sh +0 -878
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/server 2.py +0 -733
- package/src/storage_router 2.py +0 -32
- package/src/tools_coordination 2.py +0 -102
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_learnings 2.py +0 -220
- package/src/tools_menu 2.py +0 -227
- package/src/tools_reminders 2.py +0 -86
- package/src/tools_reminders_crud 2.py +0 -159
- package/src/tools_sessions 2.py +0 -476
- package/src/tools_task_history 2.py +0 -57
- package/templates/CLAUDE.md 2.template +0 -63
- package/templates/openclaw 2.json +0 -13
- package/tests/__init__ 2.py +0 -0
- package/tests/__init__.py +0 -0
- package/tests/conftest 2.py +0 -71
- package/tests/conftest.py +0 -71
- package/tests/test_cognitive 2.py +0 -205
- package/tests/test_cognitive.py +0 -205
- package/tests/test_knowledge_graph 2.py +0 -140
- package/tests/test_knowledge_graph.py +0 -140
- package/tests/test_migrations 2.py +0 -137
- package/tests/test_migrations.py +0 -137
|
@@ -1,949 +0,0 @@
|
|
|
1
|
-
"""NEXO Cognitive — Search, retrieval, ranking."""
|
|
2
|
-
import math
|
|
3
|
-
import sqlite3
|
|
4
|
-
import numpy as np
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
from cognitive._core import _get_db, embed, cosine_similarity, _blob_to_array, _array_to_blob, _get_model, _get_reranker, rerank_results, EMBEDDING_DIM
|
|
7
|
-
|
|
8
|
-
def bm25_search(query_text: str, stores: str = "both", top_k: int = 20,
|
|
9
|
-
source_type_filter: str = "") -> list[dict]:
|
|
10
|
-
"""BM25 keyword search using SQLite FTS5. Returns ranked results by relevance."""
|
|
11
|
-
db = _get_db()
|
|
12
|
-
results = []
|
|
13
|
-
|
|
14
|
-
# Sanitize query for FTS5 (escape special chars, use OR for multi-word)
|
|
15
|
-
words = [w.strip() for w in query_text.split() if w.strip() and len(w.strip()) > 1]
|
|
16
|
-
if not words:
|
|
17
|
-
return []
|
|
18
|
-
fts_query = " OR ".join(f'"{w}"' for w in words)
|
|
19
|
-
|
|
20
|
-
for store in ("stm", "ltm"):
|
|
21
|
-
if stores == "stm" and store == "ltm":
|
|
22
|
-
continue
|
|
23
|
-
if stores == "ltm" and store == "stm":
|
|
24
|
-
continue
|
|
25
|
-
|
|
26
|
-
table = f"{store}_memories"
|
|
27
|
-
fts_table = f"{store}_fts"
|
|
28
|
-
|
|
29
|
-
try:
|
|
30
|
-
sql = f"""
|
|
31
|
-
SELECT m.id, m.content, m.source_type, m.source_id, m.source_title,
|
|
32
|
-
m.domain, m.created_at, m.strength, m.access_count
|
|
33
|
-
FROM {fts_table}
|
|
34
|
-
JOIN {table} m ON m.id = {fts_table}.rowid
|
|
35
|
-
WHERE {fts_table} MATCH ?
|
|
36
|
-
"""
|
|
37
|
-
params = [fts_query]
|
|
38
|
-
|
|
39
|
-
if source_type_filter:
|
|
40
|
-
sql += " AND m.source_type = ?"
|
|
41
|
-
params.append(source_type_filter)
|
|
42
|
-
|
|
43
|
-
if store == "stm":
|
|
44
|
-
sql += " AND m.promoted_to_ltm = 0"
|
|
45
|
-
else:
|
|
46
|
-
sql += " AND m.is_dormant = 0"
|
|
47
|
-
|
|
48
|
-
sql += f" ORDER BY {fts_table}.rank LIMIT ?"
|
|
49
|
-
params.append(top_k)
|
|
50
|
-
|
|
51
|
-
rows = db.execute(sql, params).fetchall()
|
|
52
|
-
|
|
53
|
-
for rank_pos, row in enumerate(rows):
|
|
54
|
-
results.append({
|
|
55
|
-
"store": store,
|
|
56
|
-
"id": row["id"],
|
|
57
|
-
"content": row["content"],
|
|
58
|
-
"source_type": row["source_type"],
|
|
59
|
-
"source_id": row["source_id"],
|
|
60
|
-
"source_title": row["source_title"],
|
|
61
|
-
"domain": row["domain"],
|
|
62
|
-
"created_at": row["created_at"],
|
|
63
|
-
"strength": row["strength"],
|
|
64
|
-
"access_count": row["access_count"],
|
|
65
|
-
"bm25_rank": rank_pos + 1,
|
|
66
|
-
"lifecycle_state": "active",
|
|
67
|
-
})
|
|
68
|
-
except Exception:
|
|
69
|
-
# FTS5 table might not exist yet or query syntax error
|
|
70
|
-
pass
|
|
71
|
-
|
|
72
|
-
return results
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _rrf_fuse(vector_results: list[dict], bm25_results: list[dict],
|
|
76
|
-
k: int = 60, alpha: float = 0.7) -> list[dict]:
|
|
77
|
-
"""Reciprocal Rank Fusion: merge vector and BM25 results.
|
|
78
|
-
|
|
79
|
-
Unlike the old version that only boosted vector-found results, this now
|
|
80
|
-
ALSO ADDS BM25-only results. This is critical for vocabulary mismatches
|
|
81
|
-
where semantic search misses but keyword search finds the right memory
|
|
82
|
-
(e.g., user says 'backend', memory contains 'FastAPI dashboard localhost:6174').
|
|
83
|
-
|
|
84
|
-
RRF score = alpha * 1/(k + vec_rank) + (1-alpha) * 1/(k + bm25_rank)
|
|
85
|
-
Items found by only one source get a penalty rank for the missing source.
|
|
86
|
-
"""
|
|
87
|
-
# Build lookups by (store, id)
|
|
88
|
-
vec_lookup = {}
|
|
89
|
-
for rank, r in enumerate(vector_results):
|
|
90
|
-
key = (r["store"], r["id"])
|
|
91
|
-
vec_lookup[key] = (rank + 1, r)
|
|
92
|
-
|
|
93
|
-
bm25_lookup = {}
|
|
94
|
-
for rank, r in enumerate(bm25_results):
|
|
95
|
-
key = (r["store"], r["id"])
|
|
96
|
-
if key not in bm25_lookup: # keep best rank
|
|
97
|
-
bm25_lookup[key] = (rank + 1, r)
|
|
98
|
-
|
|
99
|
-
# Merge all unique keys
|
|
100
|
-
all_keys = set(vec_lookup.keys()) | set(bm25_lookup.keys())
|
|
101
|
-
miss_rank = max(len(vector_results), len(bm25_results)) + 10 # penalty rank for missing source
|
|
102
|
-
|
|
103
|
-
fused = []
|
|
104
|
-
for key in all_keys:
|
|
105
|
-
vec_rank, vec_result = vec_lookup.get(key, (miss_rank, None))
|
|
106
|
-
bm25_rank, bm25_result = bm25_lookup.get(key, (miss_rank, None))
|
|
107
|
-
|
|
108
|
-
# Use whichever result has the data
|
|
109
|
-
base = vec_result if vec_result else bm25_result
|
|
110
|
-
result = base.copy()
|
|
111
|
-
|
|
112
|
-
rrf_score = alpha * (1.0 / (k + vec_rank)) + (1 - alpha) * (1.0 / (k + bm25_rank))
|
|
113
|
-
|
|
114
|
-
# If we have the original cosine score, blend it in to preserve semantic confidence
|
|
115
|
-
if vec_result and "score" in vec_result:
|
|
116
|
-
# Weighted blend: cosine for confidence + RRF for ranking boost
|
|
117
|
-
rrf_normalized = min(1.0, rrf_score * k) # normalize to 0-1 range
|
|
118
|
-
result["score"] = 0.7 * vec_result["score"] + 0.3 * rrf_normalized
|
|
119
|
-
else:
|
|
120
|
-
# BM25-only result: use RRF score scaled to ~0.3-0.7 range
|
|
121
|
-
result["score"] = min(0.75, rrf_score * k)
|
|
122
|
-
|
|
123
|
-
result["bm25_boosted"] = key in bm25_lookup
|
|
124
|
-
result["bm25_only"] = key not in vec_lookup
|
|
125
|
-
result["rrf_score"] = rrf_score
|
|
126
|
-
fused.append(result)
|
|
127
|
-
|
|
128
|
-
# Sort by score descending
|
|
129
|
-
fused.sort(key=lambda x: x["score"], reverse=True)
|
|
130
|
-
return fused
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
# ── Temporal Boosting ────────────────────────────────────────────────
|
|
134
|
-
# Recent memories get a bounded additive boost at query time.
|
|
135
|
-
# Design from multi-AI debate (GPT-5.4 + Gemini 3.1 Pro + Claude Opus 4.6):
|
|
136
|
-
# - Additive, not multiplicative (preserves old strong matches)
|
|
137
|
-
# - Relevance-gated (only boost if already above threshold)
|
|
138
|
-
# - Query-adaptive alpha (operational queries get more boost)
|
|
139
|
-
|
|
140
|
-
# Operational keywords that suggest the user wants recent/active things
|
|
141
|
-
_OPERATIONAL_CUES = frozenset({
|
|
142
|
-
"current", "latest", "now", "running", "active", "today", "yesterday",
|
|
143
|
-
"tonight", "backend", "server", "dashboard", "service", "localhost",
|
|
144
|
-
"anoche", "ayer", "ahora", "actual", "corriendo", "activo", "hoy",
|
|
145
|
-
"madrugada", "esta mañana", "last night", "this morning",
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
# Historical keywords that suggest the user wants old things
|
|
149
|
-
_HISTORICAL_CUES = frozenset({
|
|
150
|
-
"ago", "month", "months", "year", "years", "previous", "earlier",
|
|
151
|
-
"cuando", "hace", "meses", "año", "anterior", "antes",
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def _apply_temporal_boost(results: list[dict], query_text: str) -> list[dict]:
|
|
156
|
-
"""Apply bounded temporal boost to retrieval results.
|
|
157
|
-
|
|
158
|
-
Recent memories (hours/days) get a small additive bonus, but only if they
|
|
159
|
-
already have a reasonable relevance score (gated at 0.45). This prevents
|
|
160
|
-
recent junk from outranking strong old matches.
|
|
161
|
-
|
|
162
|
-
The boost decays with a 3-day half-life:
|
|
163
|
-
boost = alpha * exp(-ln(2) * age_days / 3)
|
|
164
|
-
|
|
165
|
-
Alpha is query-adaptive:
|
|
166
|
-
- Operational queries ('backend', 'active', 'today'): alpha = 0.06
|
|
167
|
-
- Default queries: alpha = 0.02
|
|
168
|
-
- Historical queries ('ago', 'months', 'year'): alpha = 0.0 (disabled)
|
|
169
|
-
"""
|
|
170
|
-
if not results:
|
|
171
|
-
return results
|
|
172
|
-
|
|
173
|
-
# Determine alpha based on query intent
|
|
174
|
-
query_tokens = set(query_text.lower().split())
|
|
175
|
-
if query_tokens & _HISTORICAL_CUES:
|
|
176
|
-
return results # No temporal boost for historical queries
|
|
177
|
-
elif query_tokens & _OPERATIONAL_CUES:
|
|
178
|
-
alpha = 0.06
|
|
179
|
-
else:
|
|
180
|
-
alpha = 0.02
|
|
181
|
-
|
|
182
|
-
now = datetime.now()
|
|
183
|
-
ln2 = math.log(2)
|
|
184
|
-
half_life_days = 3.0
|
|
185
|
-
|
|
186
|
-
for r in results:
|
|
187
|
-
# Only boost if already reasonably relevant (relevance gate)
|
|
188
|
-
if r.get("score", 0) < 0.45:
|
|
189
|
-
continue
|
|
190
|
-
|
|
191
|
-
# Calculate age in days
|
|
192
|
-
created_str = r.get("created_at", "")
|
|
193
|
-
if not created_str:
|
|
194
|
-
continue
|
|
195
|
-
try:
|
|
196
|
-
created = datetime.fromisoformat(created_str.replace("Z", "+00:00").replace("+00:00", ""))
|
|
197
|
-
age_days = max(0, (now - created).total_seconds() / 86400)
|
|
198
|
-
except (ValueError, TypeError):
|
|
199
|
-
continue
|
|
200
|
-
|
|
201
|
-
# Bounded exponential decay boost
|
|
202
|
-
boost = alpha * math.exp(-ln2 * age_days / half_life_days)
|
|
203
|
-
|
|
204
|
-
# Apply boost (capped at 0.95 — reserve 1.0 for exact matches only)
|
|
205
|
-
r["score"] = min(0.95, r["score"] + boost)
|
|
206
|
-
if boost > 0.001:
|
|
207
|
-
r["temporal_boost"] = round(boost, 4)
|
|
208
|
-
|
|
209
|
-
return results
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
# ============================================================================
|
|
213
|
-
# FEATURE 0.5: Knowledge Graph Boost
|
|
214
|
-
# Memories connected to more KG nodes (files, areas, other learnings) are
|
|
215
|
-
# more structurally important. Apply a small additive boost proportional to
|
|
216
|
-
# their connection count. This bridges the vector (semantic) and graph
|
|
217
|
-
# (structural) worlds.
|
|
218
|
-
# ============================================================================
|
|
219
|
-
|
|
220
|
-
def _kg_boost_results(results: list[dict], max_boost: float = 0.08) -> list[dict]:
|
|
221
|
-
"""Boost search results based on Knowledge Graph connectivity.
|
|
222
|
-
|
|
223
|
-
For each result whose source (learning, change, decision, entity) has a
|
|
224
|
-
corresponding KG node, add a logarithmic boost based on connection count.
|
|
225
|
-
More connected memories = more structurally important = slight score lift.
|
|
226
|
-
|
|
227
|
-
Boost formula: min(max_boost, 0.015 * log2(connections + 1))
|
|
228
|
-
- 1 connection → +0.015
|
|
229
|
-
- 4 connections → +0.034
|
|
230
|
-
- 16 connections → +0.060
|
|
231
|
-
- 32+ connections → capped at +0.08
|
|
232
|
-
"""
|
|
233
|
-
if not results:
|
|
234
|
-
return results
|
|
235
|
-
|
|
236
|
-
try:
|
|
237
|
-
db = _get_db()
|
|
238
|
-
except Exception:
|
|
239
|
-
return results
|
|
240
|
-
|
|
241
|
-
# Collect KG node refs from results
|
|
242
|
-
# KG node_refs use format "learning:212", "change:39", "decision:14"
|
|
243
|
-
# Memory source_ids use format "L464", "C39", "D14" or raw IDs
|
|
244
|
-
_prefix_map = {"learning": "L", "change": "C", "decision": "D", "entity": "E"}
|
|
245
|
-
ref_map = {} # node_ref -> list of result indices
|
|
246
|
-
for i, r in enumerate(results):
|
|
247
|
-
source_type = r.get("source_type", "")
|
|
248
|
-
source_id = r.get("source_id", "")
|
|
249
|
-
if not source_type or not source_id:
|
|
250
|
-
continue
|
|
251
|
-
# Convert memory source_id to KG node_ref
|
|
252
|
-
prefix = _prefix_map.get(source_type, "")
|
|
253
|
-
if prefix and source_id.startswith(prefix):
|
|
254
|
-
numeric_id = source_id[len(prefix):]
|
|
255
|
-
node_ref = f"{source_type}:{numeric_id}"
|
|
256
|
-
else:
|
|
257
|
-
node_ref = f"{source_type}:{source_id}"
|
|
258
|
-
ref_map.setdefault(node_ref, []).append(i)
|
|
259
|
-
|
|
260
|
-
if not ref_map:
|
|
261
|
-
return results
|
|
262
|
-
|
|
263
|
-
# Batch query: get connection counts for all relevant KG nodes
|
|
264
|
-
try:
|
|
265
|
-
placeholders = ",".join(["?"] * len(ref_map))
|
|
266
|
-
rows = db.execute(f"""
|
|
267
|
-
SELECT n.node_ref, COUNT(e.id) as connections
|
|
268
|
-
FROM kg_nodes n
|
|
269
|
-
LEFT JOIN kg_edges e ON (e.source_id = n.id OR e.target_id = n.id)
|
|
270
|
-
AND e.valid_until IS NULL
|
|
271
|
-
WHERE n.node_ref IN ({placeholders})
|
|
272
|
-
GROUP BY n.id
|
|
273
|
-
""", list(ref_map.keys())).fetchall()
|
|
274
|
-
except Exception:
|
|
275
|
-
return results
|
|
276
|
-
|
|
277
|
-
# Apply boosts
|
|
278
|
-
for row in rows:
|
|
279
|
-
node_ref = row["node_ref"]
|
|
280
|
-
connections = row["connections"]
|
|
281
|
-
if connections <= 0:
|
|
282
|
-
continue
|
|
283
|
-
boost = min(max_boost, 0.015 * math.log2(connections + 1))
|
|
284
|
-
for idx in ref_map.get(node_ref, []):
|
|
285
|
-
r = results[idx]
|
|
286
|
-
if r.get("score", 0) >= 0.45: # Same relevance gate as temporal
|
|
287
|
-
r["score"] = min(0.95, r["score"] + boost)
|
|
288
|
-
r["kg_boost"] = round(boost, 4)
|
|
289
|
-
r["kg_connections"] = connections
|
|
290
|
-
|
|
291
|
-
return results
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
# ============================================================================
|
|
295
|
-
# FEATURE 1: HyDE Query Expansion (adapted from Vestige hyde.rs)
|
|
296
|
-
# Template-based Hypothetical Document Embeddings for improved search recall.
|
|
297
|
-
# Classifies query intent, generates 3-5 semantic variants, embeds all,
|
|
298
|
-
# averages into centroid embedding for broader semantic coverage.
|
|
299
|
-
# ============================================================================
|
|
300
|
-
|
|
301
|
-
def _classify_query_intent(query: str) -> str:
|
|
302
|
-
"""Classify query intent into one of 6 categories (Vestige-style)."""
|
|
303
|
-
lower = query.lower().strip()
|
|
304
|
-
if lower.startswith(("how to", "how do", "steps", "cómo")):
|
|
305
|
-
return "howto"
|
|
306
|
-
if lower.startswith(("what is", "what are", "define", "explain", "qué es")):
|
|
307
|
-
return "definition"
|
|
308
|
-
if lower.startswith(("why", "por qué")) or "reason" in lower or "porque" in lower:
|
|
309
|
-
return "reasoning"
|
|
310
|
-
if lower.startswith(("when", "cuándo")) or "date" in lower or "timeline" in lower or "fecha" in lower:
|
|
311
|
-
return "temporal"
|
|
312
|
-
if any(c in query for c in ("(", "{", "::", "def ", "class ", "fn ", "function ")):
|
|
313
|
-
return "technical"
|
|
314
|
-
return "lookup"
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def _expand_query_variants(query: str) -> list[str]:
|
|
318
|
-
"""Generate 3-5 expanded query variants based on intent (Vestige-style)."""
|
|
319
|
-
intent = _classify_query_intent(query)
|
|
320
|
-
clean = query.strip().rstrip("?.!")
|
|
321
|
-
variants = [query]
|
|
322
|
-
|
|
323
|
-
templates = {
|
|
324
|
-
"definition": [
|
|
325
|
-
f"{clean} is a concept that involves",
|
|
326
|
-
f"The definition of {clean} in the context of this project",
|
|
327
|
-
f"{clean} refers to a type of",
|
|
328
|
-
],
|
|
329
|
-
"howto": [
|
|
330
|
-
f"The steps to {clean} are as follows",
|
|
331
|
-
f"To accomplish {clean}, you need to",
|
|
332
|
-
f"A guide for {clean} including",
|
|
333
|
-
],
|
|
334
|
-
"reasoning": [
|
|
335
|
-
f"The reason {clean} is because",
|
|
336
|
-
f"{clean} happens due to the following factors",
|
|
337
|
-
f"The explanation for {clean} involves",
|
|
338
|
-
],
|
|
339
|
-
"temporal": [
|
|
340
|
-
f"{clean} occurred at a specific time",
|
|
341
|
-
f"The timeline of {clean} shows",
|
|
342
|
-
f"Events related to {clean} in chronological order",
|
|
343
|
-
],
|
|
344
|
-
"lookup": [
|
|
345
|
-
f"Information about {clean} including details",
|
|
346
|
-
f"{clean} is related to the following topics",
|
|
347
|
-
f"Key facts about {clean}",
|
|
348
|
-
f"Previously we handled {clean} by",
|
|
349
|
-
],
|
|
350
|
-
"technical": [
|
|
351
|
-
f"{clean} implementation details and code",
|
|
352
|
-
f"Code pattern for {clean}",
|
|
353
|
-
],
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
variants.extend(templates.get(intent, templates["lookup"]))
|
|
357
|
-
return variants
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
def hyde_expand_query(query: str) -> np.ndarray:
|
|
361
|
-
"""HyDE: embed expanded query variants and return their centroid.
|
|
362
|
-
|
|
363
|
-
Instead of embedding just the raw query, generates 3-5 semantic
|
|
364
|
-
variants and returns the averaged (centroid) embedding. This gives
|
|
365
|
-
~60% of full LLM-based HyDE quality with zero latency overhead.
|
|
366
|
-
|
|
367
|
-
Based on Vestige's template-based HyDE (hyde.rs) and the original
|
|
368
|
-
HyDE paper (Gao et al., 2022).
|
|
369
|
-
"""
|
|
370
|
-
variants = _expand_query_variants(query)
|
|
371
|
-
model = _get_model()
|
|
372
|
-
embeddings = list(model.embed(variants))
|
|
373
|
-
arrays = [np.array(e, dtype=np.float32) for e in embeddings]
|
|
374
|
-
|
|
375
|
-
centroid = np.mean(arrays, axis=0).astype(np.float32)
|
|
376
|
-
norm = np.linalg.norm(centroid)
|
|
377
|
-
if norm > 0:
|
|
378
|
-
centroid = centroid / norm
|
|
379
|
-
|
|
380
|
-
return centroid
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
# ============================================================================
|
|
384
|
-
# FEATURE 2: Spreading Activation / Co-Activation Reinforcement
|
|
385
|
-
# Adapted from Vestige spreading_activation.rs and ClawMem store.ts
|
|
386
|
-
# Memories retrieved together get co-activation links that boost
|
|
387
|
-
# future retrievals of associated memories.
|
|
388
|
-
# ============================================================================
|
|
389
|
-
|
|
390
|
-
CO_ACTIVATION_DECAY = 0.7
|
|
391
|
-
CO_ACTIVATION_BOOST = 0.05
|
|
392
|
-
CO_ACTIVATION_MIN_STRENGTH = 0.1
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
def _canonical_co_id(store: str, mid: int) -> int:
|
|
396
|
-
"""Create a canonical hash ID for co-activation tracking."""
|
|
397
|
-
return hash(f"{store}:{mid}") % (2**31)
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
def record_co_activation(memory_ids: list[tuple[str, int]]):
|
|
401
|
-
"""Record co-activation between all pairs of retrieved memories.
|
|
402
|
-
|
|
403
|
-
Called after search returns results. Memories surfaced together
|
|
404
|
-
get their co-activation links reinforced (ClawMem pattern).
|
|
405
|
-
"""
|
|
406
|
-
if len(memory_ids) < 2:
|
|
407
|
-
return
|
|
408
|
-
|
|
409
|
-
db = _get_db()
|
|
410
|
-
now = datetime.utcnow().isoformat()
|
|
411
|
-
|
|
412
|
-
hashes = [_canonical_co_id(store, mid) for store, mid in memory_ids]
|
|
413
|
-
|
|
414
|
-
for i in range(len(hashes)):
|
|
415
|
-
for j in range(i + 1, len(hashes)):
|
|
416
|
-
a, b = min(hashes[i], hashes[j]), max(hashes[i], hashes[j])
|
|
417
|
-
db.execute("""
|
|
418
|
-
INSERT INTO co_activation (memory_a_id, memory_b_id, strength, co_access_count, last_co_access)
|
|
419
|
-
VALUES (?, ?, 1.0, 1, ?)
|
|
420
|
-
ON CONFLICT(memory_a_id, memory_b_id) DO UPDATE SET
|
|
421
|
-
strength = MIN(5.0, strength + 0.3),
|
|
422
|
-
co_access_count = co_access_count + 1,
|
|
423
|
-
last_co_access = excluded.last_co_access
|
|
424
|
-
""", (a, b, now))
|
|
425
|
-
|
|
426
|
-
db.commit()
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
def _get_co_activated_neighbors(memory_ids: list[tuple[str, int]], depth: int = 1) -> dict[int, float]:
|
|
430
|
-
"""Get co-activated neighbor boosts for a set of memory IDs.
|
|
431
|
-
|
|
432
|
-
Returns {canonical_hash: boost_score} for neighbor memories.
|
|
433
|
-
Uses BFS spreading with decay per hop (Vestige pattern).
|
|
434
|
-
"""
|
|
435
|
-
db = _get_db()
|
|
436
|
-
boosts = {}
|
|
437
|
-
|
|
438
|
-
source_hashes = set(_canonical_co_id(s, m) for s, m in memory_ids)
|
|
439
|
-
current_level = list(source_hashes)
|
|
440
|
-
|
|
441
|
-
for hop in range(depth):
|
|
442
|
-
decay = CO_ACTIVATION_DECAY ** (hop + 1)
|
|
443
|
-
next_level = []
|
|
444
|
-
|
|
445
|
-
for src_hash in current_level:
|
|
446
|
-
rows = db.execute("""
|
|
447
|
-
SELECT memory_a_id, memory_b_id, strength FROM co_activation
|
|
448
|
-
WHERE (memory_a_id = ? OR memory_b_id = ?) AND strength >= ?
|
|
449
|
-
""", (src_hash, src_hash, CO_ACTIVATION_MIN_STRENGTH)).fetchall()
|
|
450
|
-
|
|
451
|
-
for row in rows:
|
|
452
|
-
neighbor_id = row["memory_b_id"] if row["memory_a_id"] == src_hash else row["memory_a_id"]
|
|
453
|
-
if neighbor_id in source_hashes:
|
|
454
|
-
continue
|
|
455
|
-
|
|
456
|
-
boost = row["strength"] * decay * CO_ACTIVATION_BOOST
|
|
457
|
-
if neighbor_id not in boosts or boosts[neighbor_id] < boost:
|
|
458
|
-
boosts[neighbor_id] = boost
|
|
459
|
-
next_level.append(neighbor_id)
|
|
460
|
-
|
|
461
|
-
current_level = next_level
|
|
462
|
-
|
|
463
|
-
return boosts
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
# ============================================================================
|
|
467
|
-
# FEATURE 3: Prospective Memory (adapted from Vestige prospective_memory.rs)
|
|
468
|
-
# "Remember to do X when Y happens" — intention-based triggers that fire
|
|
469
|
-
# when incoming text matches a pattern (keyword or semantic).
|
|
470
|
-
# ============================================================================
|
|
471
|
-
|
|
472
|
-
def create_trigger(pattern: str, action: str, context: str = "") -> int:
|
|
473
|
-
"""Create a prospective memory trigger.
|
|
474
|
-
|
|
475
|
-
Args:
|
|
476
|
-
pattern: Keywords or phrase to match (case-insensitive, comma-separated for multiple)
|
|
477
|
-
action: What to do when the trigger fires
|
|
478
|
-
context: Optional context about why this trigger was created
|
|
479
|
-
Returns:
|
|
480
|
-
Trigger ID
|
|
481
|
-
"""
|
|
482
|
-
db = _get_db()
|
|
483
|
-
cur = db.execute(
|
|
484
|
-
"INSERT INTO prospective_triggers (trigger_pattern, action, context) VALUES (?, ?, ?)",
|
|
485
|
-
(pattern, action, context)
|
|
486
|
-
)
|
|
487
|
-
db.commit()
|
|
488
|
-
return cur.lastrowid
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
def check_triggers(text: str, use_semantic: bool = False, semantic_threshold: float = 0.7) -> list[dict]:
|
|
492
|
-
"""Check text against all armed triggers. Fires matches.
|
|
493
|
-
|
|
494
|
-
Uses keyword matching by default. If use_semantic=True, also checks
|
|
495
|
-
semantic similarity (Vestige TriggerPattern.matches pattern).
|
|
496
|
-
|
|
497
|
-
Args:
|
|
498
|
-
text: Input text to check
|
|
499
|
-
use_semantic: Also do embedding similarity matching
|
|
500
|
-
semantic_threshold: Min cosine similarity for semantic match
|
|
501
|
-
Returns:
|
|
502
|
-
List of fired triggers with actions
|
|
503
|
-
"""
|
|
504
|
-
if not text or not text.strip():
|
|
505
|
-
return []
|
|
506
|
-
|
|
507
|
-
db = _get_db()
|
|
508
|
-
armed = db.execute(
|
|
509
|
-
"SELECT * FROM prospective_triggers WHERE status = 'armed'"
|
|
510
|
-
).fetchall()
|
|
511
|
-
|
|
512
|
-
if not armed:
|
|
513
|
-
return []
|
|
514
|
-
|
|
515
|
-
text_lower = text.lower()
|
|
516
|
-
text_vec = None
|
|
517
|
-
if use_semantic:
|
|
518
|
-
text_vec = embed(text)
|
|
519
|
-
|
|
520
|
-
fired = []
|
|
521
|
-
now = datetime.utcnow().isoformat()
|
|
522
|
-
|
|
523
|
-
for trigger in armed:
|
|
524
|
-
pattern = trigger["trigger_pattern"].lower()
|
|
525
|
-
matched = False
|
|
526
|
-
match_type = ""
|
|
527
|
-
|
|
528
|
-
# Keyword match (comma-separated OR)
|
|
529
|
-
keywords = [kw.strip() for kw in pattern.split(",") if kw.strip()]
|
|
530
|
-
if any(kw in text_lower for kw in keywords):
|
|
531
|
-
matched = True
|
|
532
|
-
match_type = "keyword"
|
|
533
|
-
|
|
534
|
-
# Semantic match (optional, more expensive)
|
|
535
|
-
if not matched and use_semantic and text_vec is not None:
|
|
536
|
-
pattern_vec = embed(trigger["trigger_pattern"])
|
|
537
|
-
sim = cosine_similarity(text_vec, pattern_vec)
|
|
538
|
-
if sim >= semantic_threshold:
|
|
539
|
-
matched = True
|
|
540
|
-
match_type = f"semantic({sim:.3f})"
|
|
541
|
-
|
|
542
|
-
if matched:
|
|
543
|
-
db.execute(
|
|
544
|
-
"UPDATE prospective_triggers SET status = 'fired', fired_at = ? WHERE id = ?",
|
|
545
|
-
(now, trigger["id"])
|
|
546
|
-
)
|
|
547
|
-
fired.append({
|
|
548
|
-
"id": trigger["id"],
|
|
549
|
-
"pattern": trigger["trigger_pattern"],
|
|
550
|
-
"action": trigger["action"],
|
|
551
|
-
"context": trigger["context"],
|
|
552
|
-
"match_type": match_type,
|
|
553
|
-
"created_at": trigger["created_at"],
|
|
554
|
-
})
|
|
555
|
-
|
|
556
|
-
if fired:
|
|
557
|
-
db.commit()
|
|
558
|
-
|
|
559
|
-
return fired
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
def list_triggers(status: str = "armed") -> list[dict]:
|
|
563
|
-
"""List prospective triggers filtered by status."""
|
|
564
|
-
db = _get_db()
|
|
565
|
-
if status == "all":
|
|
566
|
-
rows = db.execute("SELECT * FROM prospective_triggers ORDER BY created_at DESC").fetchall()
|
|
567
|
-
else:
|
|
568
|
-
rows = db.execute(
|
|
569
|
-
"SELECT * FROM prospective_triggers WHERE status = ? ORDER BY created_at DESC",
|
|
570
|
-
(status,)
|
|
571
|
-
).fetchall()
|
|
572
|
-
return [dict(row) for row in rows]
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
def delete_trigger(trigger_id: int) -> str:
|
|
576
|
-
"""Delete a prospective trigger by ID."""
|
|
577
|
-
db = _get_db()
|
|
578
|
-
cur = db.execute("DELETE FROM prospective_triggers WHERE id = ?", (trigger_id,))
|
|
579
|
-
db.commit()
|
|
580
|
-
return f"Trigger #{trigger_id} {'deleted' if cur.rowcount else 'not found'}."
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
def rearm_trigger(trigger_id: int) -> str:
|
|
584
|
-
"""Re-arm a fired trigger so it can fire again."""
|
|
585
|
-
db = _get_db()
|
|
586
|
-
cur = db.execute(
|
|
587
|
-
"UPDATE prospective_triggers SET status = 'armed', fired_at = NULL WHERE id = ?",
|
|
588
|
-
(trigger_id,)
|
|
589
|
-
)
|
|
590
|
-
db.commit()
|
|
591
|
-
return f"Trigger #{trigger_id} {'re-armed' if cur.rowcount else 'not found'}."
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
def _auto_restore_snoozed(db: sqlite3.Connection):
|
|
595
|
-
"""Restore snoozed memories whose snooze_until date has passed."""
|
|
596
|
-
now = datetime.utcnow().isoformat()
|
|
597
|
-
for table in ("stm_memories", "ltm_memories"):
|
|
598
|
-
db.execute(
|
|
599
|
-
f"UPDATE {table} SET lifecycle_state = 'active', snooze_until = NULL "
|
|
600
|
-
f"WHERE lifecycle_state = 'snoozed' AND snooze_until IS NOT NULL AND snooze_until <= ?",
|
|
601
|
-
(now,)
|
|
602
|
-
)
|
|
603
|
-
db.commit()
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
def _rehearse_results(results: list[dict], skip_ids: set = None):
|
|
607
|
-
"""Update strength and access_count for retrieved results (rehearsal)."""
|
|
608
|
-
if not results:
|
|
609
|
-
return
|
|
610
|
-
db = _get_db()
|
|
611
|
-
now = datetime.utcnow().isoformat()
|
|
612
|
-
skip = skip_ids or set()
|
|
613
|
-
for r in results:
|
|
614
|
-
if (r["store"], r["id"]) in skip:
|
|
615
|
-
continue
|
|
616
|
-
table = "stm_memories" if r["store"] == "stm" else "ltm_memories"
|
|
617
|
-
db.execute(
|
|
618
|
-
f"UPDATE {table} SET strength = MIN(1.0, strength + 0.08), access_count = access_count + 1, last_accessed = ? WHERE id = ?",
|
|
619
|
-
(now, r["id"])
|
|
620
|
-
)
|
|
621
|
-
db.commit()
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
def search(
|
|
625
|
-
query_text: str,
|
|
626
|
-
top_k: int = 10,
|
|
627
|
-
min_score: float = 0.5,
|
|
628
|
-
stores: str = "both",
|
|
629
|
-
exclude_dormant: bool = True,
|
|
630
|
-
rehearse: bool = True,
|
|
631
|
-
source_type_filter: str = "",
|
|
632
|
-
include_archived: bool = False,
|
|
633
|
-
use_hyde: bool = False,
|
|
634
|
-
hybrid: bool = True,
|
|
635
|
-
hybrid_alpha: float = 0.6,
|
|
636
|
-
spreading_depth: int = 0,
|
|
637
|
-
decompose: bool = True,
|
|
638
|
-
exclude_dreams: bool = True,
|
|
639
|
-
) -> list[dict]:
|
|
640
|
-
"""Full vector search across STM and/or LTM with rehearsal and dormant reactivation.
|
|
641
|
-
|
|
642
|
-
Args:
|
|
643
|
-
use_hyde: If True, use HyDE query expansion for richer embedding (default False)
|
|
644
|
-
spreading_depth: If >0, fetch co-activated neighbors and boost their scores (default 0)
|
|
645
|
-
exclude_dreams: If True (default), exclude dream_insight memories from results.
|
|
646
|
-
Dream insights are 21% of LTM and dilute search precision.
|
|
647
|
-
Set to False only when explicitly looking for cross-domain patterns.
|
|
648
|
-
hybrid: If True, boost results with BM25 keyword matches (default True)
|
|
649
|
-
hybrid_alpha: Weight for vector vs BM25. Higher = more vector. (default 0.6)
|
|
650
|
-
decompose: If True, decompose complex queries into sub-queries for better multi-hop (default True)
|
|
651
|
-
"""
|
|
652
|
-
# Multi-query decomposition: for complex questions, search sub-parts and merge
|
|
653
|
-
if decompose and query_text:
|
|
654
|
-
_connectors = [" after ", " before ", " because ", " and then ", " when ", " while "]
|
|
655
|
-
for conn in _connectors:
|
|
656
|
-
if conn in query_text.lower():
|
|
657
|
-
parts = query_text.lower().split(conn, 1)
|
|
658
|
-
if len(parts) == 2 and len(parts[0]) > 10 and len(parts[1]) > 10:
|
|
659
|
-
# Search each sub-query separately, merge results by max score
|
|
660
|
-
all_results = {}
|
|
661
|
-
for sub_q in [query_text, parts[0].strip("? "), parts[1].strip("? ")]:
|
|
662
|
-
sub_results = search(
|
|
663
|
-
sub_q, top_k=top_k, min_score=min_score, stores=stores,
|
|
664
|
-
exclude_dormant=exclude_dormant, rehearse=False,
|
|
665
|
-
source_type_filter=source_type_filter,
|
|
666
|
-
include_archived=include_archived, use_hyde=use_hyde,
|
|
667
|
-
hybrid=hybrid, hybrid_alpha=hybrid_alpha,
|
|
668
|
-
spreading_depth=spreading_depth, decompose=False, # No recursion
|
|
669
|
-
)
|
|
670
|
-
for r in sub_results:
|
|
671
|
-
key = (r["store"], r["id"])
|
|
672
|
-
if key not in all_results or r["score"] > all_results[key]["score"]:
|
|
673
|
-
all_results[key] = r
|
|
674
|
-
merged = sorted(all_results.values(), key=lambda x: x["score"], reverse=True)[:top_k]
|
|
675
|
-
if rehearse:
|
|
676
|
-
_rehearse_results(merged)
|
|
677
|
-
return merged
|
|
678
|
-
|
|
679
|
-
db = _get_db()
|
|
680
|
-
|
|
681
|
-
# Detect temporal queries — boost results with temporal_date
|
|
682
|
-
_temporal_keywords = {"when", "date", "time", "first", "last", "before", "after",
|
|
683
|
-
"cuándo", "cuando", "fecha", "primero", "último", "antes", "después"}
|
|
684
|
-
query_lower = query_text.lower().split()
|
|
685
|
-
is_temporal_query = bool(_temporal_keywords & set(query_lower))
|
|
686
|
-
|
|
687
|
-
if use_hyde:
|
|
688
|
-
query_vec = hyde_expand_query(query_text)
|
|
689
|
-
else:
|
|
690
|
-
query_vec = embed(query_text)
|
|
691
|
-
if np.linalg.norm(query_vec) == 0:
|
|
692
|
-
return []
|
|
693
|
-
|
|
694
|
-
# Auto-restore snoozed memories whose snooze_until has passed
|
|
695
|
-
_auto_restore_snoozed(db)
|
|
696
|
-
|
|
697
|
-
# HNSW fast-path: use approximate nearest neighbors when available
|
|
698
|
-
_hnsw_candidates = None
|
|
699
|
-
try:
|
|
700
|
-
import hnsw_index
|
|
701
|
-
if hnsw_index.is_available() and hnsw_index.should_activate(stores):
|
|
702
|
-
_hnsw_candidates = {}
|
|
703
|
-
for s in (["stm", "ltm"] if stores == "both" else [stores]):
|
|
704
|
-
hits = hnsw_index.search(query_vec, store=s, top_k=top_k * 4)
|
|
705
|
-
if hits:
|
|
706
|
-
for db_id, score in hits:
|
|
707
|
-
_hnsw_candidates[(s, db_id)] = score
|
|
708
|
-
except Exception:
|
|
709
|
-
_hnsw_candidates = None
|
|
710
|
-
|
|
711
|
-
results = []
|
|
712
|
-
reactivated_ids = set()
|
|
713
|
-
|
|
714
|
-
# Lifecycle filter: exclude snoozed always; exclude archived unless requested
|
|
715
|
-
_lc = " AND (lifecycle_state IS NULL OR lifecycle_state = 'active' OR lifecycle_state = 'pinned'"
|
|
716
|
-
if include_archived:
|
|
717
|
-
_lc += " OR lifecycle_state = 'archived'"
|
|
718
|
-
_lc += ")"
|
|
719
|
-
|
|
720
|
-
# Search STM
|
|
721
|
-
if stores in ("both", "stm"):
|
|
722
|
-
where = "WHERE promoted_to_ltm = 0" + _lc
|
|
723
|
-
params = []
|
|
724
|
-
if source_type_filter:
|
|
725
|
-
where += " AND source_type = ?"
|
|
726
|
-
params.append(source_type_filter)
|
|
727
|
-
rows = db.execute(f"SELECT * FROM stm_memories {where}", params).fetchall()
|
|
728
|
-
|
|
729
|
-
for row in rows:
|
|
730
|
-
# HNSW fast-path: skip rows not in candidate set
|
|
731
|
-
if _hnsw_candidates is not None and ("stm", row["id"]) not in _hnsw_candidates:
|
|
732
|
-
continue
|
|
733
|
-
vec = _blob_to_array(row["embedding"])
|
|
734
|
-
score = cosine_similarity(query_vec, vec)
|
|
735
|
-
lifecycle = row["lifecycle_state"] or "active"
|
|
736
|
-
if lifecycle == "pinned":
|
|
737
|
-
score = min(1.0, score + 0.2)
|
|
738
|
-
if score >= min_score:
|
|
739
|
-
temporal = ""
|
|
740
|
-
try:
|
|
741
|
-
temporal = row["temporal_date"] or ""
|
|
742
|
-
except (IndexError, KeyError):
|
|
743
|
-
pass
|
|
744
|
-
results.append({
|
|
745
|
-
"store": "stm",
|
|
746
|
-
"id": row["id"],
|
|
747
|
-
"content": row["content"],
|
|
748
|
-
"source_type": row["source_type"],
|
|
749
|
-
"source_id": row["source_id"],
|
|
750
|
-
"source_title": row["source_title"],
|
|
751
|
-
"domain": row["domain"],
|
|
752
|
-
"created_at": row["created_at"],
|
|
753
|
-
"strength": row["strength"],
|
|
754
|
-
"access_count": row["access_count"],
|
|
755
|
-
"score": score,
|
|
756
|
-
"lifecycle_state": lifecycle,
|
|
757
|
-
"temporal_date": temporal,
|
|
758
|
-
})
|
|
759
|
-
|
|
760
|
-
# Search LTM (active)
|
|
761
|
-
if stores in ("both", "ltm"):
|
|
762
|
-
where = "WHERE is_dormant = 0" + _lc
|
|
763
|
-
params = []
|
|
764
|
-
if source_type_filter:
|
|
765
|
-
where += " AND source_type = ?"
|
|
766
|
-
params.append(source_type_filter)
|
|
767
|
-
if exclude_dreams and not source_type_filter:
|
|
768
|
-
where += " AND source_type != 'dream_insight'"
|
|
769
|
-
rows = db.execute(f"SELECT * FROM ltm_memories {where}", params).fetchall()
|
|
770
|
-
|
|
771
|
-
for row in rows:
|
|
772
|
-
# HNSW fast-path: skip rows not in candidate set
|
|
773
|
-
if _hnsw_candidates is not None and ("ltm", row["id"]) not in _hnsw_candidates:
|
|
774
|
-
continue
|
|
775
|
-
vec = _blob_to_array(row["embedding"])
|
|
776
|
-
score = cosine_similarity(query_vec, vec)
|
|
777
|
-
lifecycle = row["lifecycle_state"] or "active"
|
|
778
|
-
if lifecycle == "pinned":
|
|
779
|
-
score = min(1.0, score + 0.2)
|
|
780
|
-
if score >= min_score:
|
|
781
|
-
results.append({
|
|
782
|
-
"store": "ltm",
|
|
783
|
-
"id": row["id"],
|
|
784
|
-
"content": row["content"],
|
|
785
|
-
"source_type": row["source_type"],
|
|
786
|
-
"source_id": row["source_id"],
|
|
787
|
-
"source_title": row["source_title"],
|
|
788
|
-
"domain": row["domain"],
|
|
789
|
-
"created_at": row["created_at"],
|
|
790
|
-
"strength": row["strength"],
|
|
791
|
-
"access_count": row["access_count"],
|
|
792
|
-
"score": score,
|
|
793
|
-
"tags": row["tags"],
|
|
794
|
-
"lifecycle_state": lifecycle,
|
|
795
|
-
})
|
|
796
|
-
|
|
797
|
-
# Check dormant LTM for reactivation
|
|
798
|
-
if stores in ("both", "ltm") and not exclude_dormant:
|
|
799
|
-
dormant_rows = db.execute("SELECT * FROM ltm_memories WHERE is_dormant = 1").fetchall()
|
|
800
|
-
for row in dormant_rows:
|
|
801
|
-
vec = _blob_to_array(row["embedding"])
|
|
802
|
-
score = cosine_similarity(query_vec, vec)
|
|
803
|
-
if score > 0.8:
|
|
804
|
-
# Reactivate
|
|
805
|
-
db.execute(
|
|
806
|
-
"UPDATE ltm_memories SET is_dormant = 0, strength = 0.5, last_accessed = datetime('now') WHERE id = ?",
|
|
807
|
-
(row["id"],)
|
|
808
|
-
)
|
|
809
|
-
reactivated_ids.add(("ltm", row["id"]))
|
|
810
|
-
results.append({
|
|
811
|
-
"store": "ltm",
|
|
812
|
-
"id": row["id"],
|
|
813
|
-
"content": row["content"],
|
|
814
|
-
"source_type": row["source_type"],
|
|
815
|
-
"source_id": row["source_id"],
|
|
816
|
-
"source_title": row["source_title"],
|
|
817
|
-
"domain": row["domain"],
|
|
818
|
-
"created_at": row["created_at"],
|
|
819
|
-
"strength": 0.5,
|
|
820
|
-
"access_count": row["access_count"],
|
|
821
|
-
"score": score,
|
|
822
|
-
"tags": row["tags"],
|
|
823
|
-
"reactivated": True,
|
|
824
|
-
})
|
|
825
|
-
if reactivated_ids:
|
|
826
|
-
db.commit()
|
|
827
|
-
|
|
828
|
-
# Hybrid search: boost vector results with BM25 keyword matches
|
|
829
|
-
if hybrid and query_text:
|
|
830
|
-
bm25_results = bm25_search(query_text, stores=stores, top_k=top_k * 4,
|
|
831
|
-
source_type_filter=source_type_filter)
|
|
832
|
-
if bm25_results:
|
|
833
|
-
results = _rrf_fuse(results, bm25_results, alpha=hybrid_alpha)
|
|
834
|
-
|
|
835
|
-
# Temporal boost: for "when" queries, boost results that have temporal_date
|
|
836
|
-
if is_temporal_query:
|
|
837
|
-
for r in results:
|
|
838
|
-
if r.get("temporal_date"):
|
|
839
|
-
r["score"] = min(0.95, r["score"] + 0.05)
|
|
840
|
-
|
|
841
|
-
# Recency temporal boost: recent memories get additive bonus (query-adaptive)
|
|
842
|
-
results = _apply_temporal_boost(results, query_text)
|
|
843
|
-
|
|
844
|
-
# Knowledge Graph structural boost: connected memories rank higher
|
|
845
|
-
results = _kg_boost_results(results)
|
|
846
|
-
|
|
847
|
-
# Sort by score descending, take top-20 for reranking
|
|
848
|
-
results.sort(key=lambda x: x.get("score", 0), reverse=True)
|
|
849
|
-
|
|
850
|
-
# Cross-encoder reranking: precise top-k from top-20 candidates
|
|
851
|
-
if len(results) > top_k:
|
|
852
|
-
results = rerank_results(query_text, results[:top_k * 4], top_k=top_k)
|
|
853
|
-
else:
|
|
854
|
-
results = results[:top_k]
|
|
855
|
-
|
|
856
|
-
# Spreading activation: boost co-activated neighbors (Feature 2)
|
|
857
|
-
co_activation_applied = False
|
|
858
|
-
if spreading_depth > 0 and results:
|
|
859
|
-
memory_ids = [(r["store"], r["id"]) for r in results]
|
|
860
|
-
neighbor_boosts = _get_co_activated_neighbors(memory_ids, depth=spreading_depth)
|
|
861
|
-
|
|
862
|
-
if neighbor_boosts:
|
|
863
|
-
co_activation_applied = True
|
|
864
|
-
# Boost existing results that are neighbors
|
|
865
|
-
existing_hashes = set()
|
|
866
|
-
for r in results:
|
|
867
|
-
co_hash = _canonical_co_id(r["store"], r["id"])
|
|
868
|
-
existing_hashes.add(co_hash)
|
|
869
|
-
if co_hash in neighbor_boosts:
|
|
870
|
-
boost = neighbor_boosts[co_hash]
|
|
871
|
-
r["score"] = min(0.95, r["score"] + boost)
|
|
872
|
-
r["co_activation_boost"] = boost
|
|
873
|
-
|
|
874
|
-
# Add neighbor memories not already in results
|
|
875
|
-
new_neighbor_hashes = set(neighbor_boosts.keys()) - existing_hashes
|
|
876
|
-
if new_neighbor_hashes:
|
|
877
|
-
for store_name, table in [("stm", "stm_memories"), ("ltm", "ltm_memories")]:
|
|
878
|
-
rows = db.execute(f"SELECT * FROM {table}").fetchall()
|
|
879
|
-
for row in rows:
|
|
880
|
-
nh = _canonical_co_id(store_name, row["id"])
|
|
881
|
-
if nh in new_neighbor_hashes:
|
|
882
|
-
boost = neighbor_boosts[nh]
|
|
883
|
-
results.append({
|
|
884
|
-
"store": store_name,
|
|
885
|
-
"id": row["id"],
|
|
886
|
-
"content": row["content"],
|
|
887
|
-
"source_type": row.get("source_type", ""),
|
|
888
|
-
"source_id": row.get("source_id", ""),
|
|
889
|
-
"tags": row.get("tags", ""),
|
|
890
|
-
"domain": row.get("domain", ""),
|
|
891
|
-
"created_at": row.get("created_at", ""),
|
|
892
|
-
"strength": row.get("strength", 0.0),
|
|
893
|
-
"access_count": row.get("access_count", 0),
|
|
894
|
-
"score": min(1.0, boost),
|
|
895
|
-
"co_activation_boost": boost,
|
|
896
|
-
"lifecycle_state": row.get("lifecycle_state", "active"),
|
|
897
|
-
})
|
|
898
|
-
new_neighbor_hashes.discard(nh)
|
|
899
|
-
|
|
900
|
-
# Re-sort after applying boosts
|
|
901
|
-
results.sort(key=lambda x: x["score"], reverse=True)
|
|
902
|
-
|
|
903
|
-
# Add rank explanations
|
|
904
|
-
for rank, r in enumerate(results, 1):
|
|
905
|
-
score = r["score"]
|
|
906
|
-
store = r["store"].upper()
|
|
907
|
-
strength = r.get("strength", 0.0)
|
|
908
|
-
access_count = r.get("access_count", 0)
|
|
909
|
-
created = r.get("created_at", "")
|
|
910
|
-
tags = r.get("tags", "")
|
|
911
|
-
reactivated = r.get("reactivated", False)
|
|
912
|
-
|
|
913
|
-
ranking_desc = "semantic_similarity"
|
|
914
|
-
if use_hyde:
|
|
915
|
-
ranking_desc = "hyde_centroid_similarity"
|
|
916
|
-
parts = [f"Ranked #{rank}: {ranking_desc}={score:.3f}"]
|
|
917
|
-
parts.append(f"store={store}, strength={strength:.2f}, accesses={access_count}")
|
|
918
|
-
if r.get("kg_boost"):
|
|
919
|
-
parts.append(f"kg_boost=+{r['kg_boost']:.3f} ({r.get('kg_connections', 0)} edges)")
|
|
920
|
-
if r.get("co_activation_boost"):
|
|
921
|
-
parts.append(f"co_activation_boost=+{r['co_activation_boost']:.3f}")
|
|
922
|
-
if created:
|
|
923
|
-
parts.append(f"created={created[:10]}")
|
|
924
|
-
if tags:
|
|
925
|
-
parts.append(f"tags={tags}")
|
|
926
|
-
if reactivated:
|
|
927
|
-
parts.append("REACTIVATED (was dormant, score>0.8 triggered revival)")
|
|
928
|
-
r["explanation"] = " | ".join(parts)
|
|
929
|
-
|
|
930
|
-
# Rehearsal: update strength and access_count for returned results
|
|
931
|
-
if rehearse and results:
|
|
932
|
-
_rehearse_results(results, skip_ids=reactivated_ids)
|
|
933
|
-
|
|
934
|
-
# Record co-activation for future spreading (Feature 2)
|
|
935
|
-
if results and len(results) >= 2:
|
|
936
|
-
try:
|
|
937
|
-
record_co_activation([(r["store"], r["id"]) for r in results])
|
|
938
|
-
except Exception:
|
|
939
|
-
pass # Non-critical — don't break search
|
|
940
|
-
|
|
941
|
-
# Log retrieval
|
|
942
|
-
top_score = results[0]["score"] if results else 0.0
|
|
943
|
-
db.execute(
|
|
944
|
-
"INSERT INTO retrieval_log (query_text, results_count, top_score) VALUES (?, ?, ?)",
|
|
945
|
-
(query_text[:500], len(results), top_score)
|
|
946
|
-
)
|
|
947
|
-
db.commit()
|
|
948
|
-
|
|
949
|
-
return results
|