nexo-brain 2.6.15 → 2.6.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +41 -5
- package/package.json +1 -1
- package/src/agent_runner.py +70 -2
- package/src/bootstrap_docs.py +2 -0
- package/src/client_sync.py +140 -0
- package/src/cognitive/__init__.py +4 -0
- package/src/cognitive/_core.py +80 -0
- package/src/cognitive/_decay.py +28 -11
- package/src/cognitive/_ingest.py +44 -22
- package/src/cognitive/_memory.py +8 -0
- package/src/cognitive/_search.py +71 -11
- package/src/dashboard/app.py +15 -8
- package/src/db/_schema.py +10 -0
- package/src/db/_sessions.py +13 -6
- package/src/doctor/providers/runtime.py +60 -5
- package/src/hooks/capture-tool-logs.sh +2 -2
- package/src/hooks/inbox-hook.sh +1 -1
- package/src/plugins/cognitive_memory.py +14 -6
- package/src/scripts/deep-sleep/collect.py +181 -0
- package/src/scripts/deep-sleep/synthesize-prompt.md +5 -0
- package/src/scripts/deep-sleep/synthesize.py +2 -0
- package/src/scripts/nexo-inbox-hook.sh +1 -1
- package/src/scripts/nexo-reflection.py +7 -4
- package/src/server.py +13 -6
- package/src/tools_sessions.py +22 -5
- package/templates/CODEX.AGENTS.md.template +2 -2
package/src/cognitive/_ingest.py
CHANGED
|
@@ -7,6 +7,7 @@ from cognitive._core import (
|
|
|
7
7
|
_get_db, embed, cosine_similarity, _blob_to_array, _array_to_blob,
|
|
8
8
|
redact_secrets, extract_temporal_date, EMBEDDING_DIM,
|
|
9
9
|
PE_GATE_REJECT, PE_GATE_REFINE, _gate_stats,
|
|
10
|
+
initial_memory_profile, rehearsal_profile_update,
|
|
10
11
|
)
|
|
11
12
|
|
|
12
13
|
|
|
@@ -76,6 +77,7 @@ def ingest(
|
|
|
76
77
|
vec = embed(clean_content)
|
|
77
78
|
blob = _array_to_blob(vec)
|
|
78
79
|
temporal = extract_temporal_date(content)
|
|
80
|
+
stability, difficulty = initial_memory_profile(source_type, store="stm")
|
|
79
81
|
|
|
80
82
|
# Auto-pin: corrections and blocking learnings get pinned (zero decay, +0.2 boost)
|
|
81
83
|
# This ensures user's corrections NEVER fade away
|
|
@@ -94,9 +96,9 @@ def ingest(
|
|
|
94
96
|
db.commit()
|
|
95
97
|
# Now actually store in STM
|
|
96
98
|
cur2 = db.execute(
|
|
97
|
-
"""INSERT INTO stm_memories (content, embedding, source_type, source_id, source_title, domain, redaction_applied, temporal_date, lifecycle_state)
|
|
98
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
99
|
-
(clean_content, blob, source_type, source_id, source_title, domain, was_redacted, temporal, _pin_lifecycle)
|
|
99
|
+
"""INSERT INTO stm_memories (content, embedding, source_type, source_id, source_title, domain, redaction_applied, temporal_date, lifecycle_state, stability, difficulty)
|
|
100
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
101
|
+
(clean_content, blob, source_type, source_id, source_title, domain, was_redacted, temporal, _pin_lifecycle, stability, difficulty)
|
|
100
102
|
)
|
|
101
103
|
db.commit()
|
|
102
104
|
_hnsw_notify_insert("stm", cur2.lastrowid, vec)
|
|
@@ -105,9 +107,9 @@ def ingest(
|
|
|
105
107
|
# skip_quarantine = direct STM (backward compatibility)
|
|
106
108
|
if skip_quarantine:
|
|
107
109
|
cur = db.execute(
|
|
108
|
-
"""INSERT INTO stm_memories (content, embedding, source_type, source_id, source_title, domain, redaction_applied, temporal_date, lifecycle_state)
|
|
109
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
110
|
-
(clean_content, blob, source_type, source_id, source_title, domain, was_redacted, temporal, _pin_lifecycle)
|
|
110
|
+
"""INSERT INTO stm_memories (content, embedding, source_type, source_id, source_title, domain, redaction_applied, temporal_date, lifecycle_state, stability, difficulty)
|
|
111
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
112
|
+
(clean_content, blob, source_type, source_id, source_title, domain, was_redacted, temporal, _pin_lifecycle, stability, difficulty)
|
|
111
113
|
)
|
|
112
114
|
db.commit()
|
|
113
115
|
_hnsw_notify_insert("stm", cur.lastrowid, vec)
|
|
@@ -246,10 +248,11 @@ def ingest_to_ltm(
|
|
|
246
248
|
was_redacted = 1 if clean_content != content else 0
|
|
247
249
|
vec = embed(clean_content)
|
|
248
250
|
blob = _array_to_blob(vec)
|
|
251
|
+
stability, difficulty = initial_memory_profile(source_type, store="ltm")
|
|
249
252
|
cur = db.execute(
|
|
250
|
-
"""INSERT INTO ltm_memories (content, embedding, source_type, source_id, source_title, domain, tags, redaction_applied)
|
|
251
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
252
|
-
(clean_content, blob, source_type, source_id, source_title, domain, tags, was_redacted)
|
|
253
|
+
"""INSERT INTO ltm_memories (content, embedding, source_type, source_id, source_title, domain, tags, redaction_applied, stability, difficulty)
|
|
254
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
255
|
+
(clean_content, blob, source_type, source_id, source_title, domain, tags, was_redacted, stability, difficulty)
|
|
253
256
|
)
|
|
254
257
|
db.commit()
|
|
255
258
|
return cur.lastrowid
|
|
@@ -267,10 +270,11 @@ def ingest_sensory(
|
|
|
267
270
|
vec = embed(clean_content)
|
|
268
271
|
blob = _array_to_blob(vec)
|
|
269
272
|
ts = created_at or datetime.utcnow().isoformat()
|
|
273
|
+
stability, difficulty = initial_memory_profile("sensory", store="stm")
|
|
270
274
|
cur = db.execute(
|
|
271
|
-
"""INSERT INTO stm_memories (content, embedding, source_type, source_id, domain, created_at, redaction_applied)
|
|
272
|
-
VALUES (?, ?, 'sensory', ?, ?, ?, ?)""",
|
|
273
|
-
(clean_content, blob, source_id, domain, ts, was_redacted)
|
|
275
|
+
"""INSERT INTO stm_memories (content, embedding, source_type, source_id, domain, created_at, redaction_applied, stability, difficulty)
|
|
276
|
+
VALUES (?, ?, 'sensory', ?, ?, ?, ?, ?, ?)""",
|
|
277
|
+
(clean_content, blob, source_id, domain, ts, was_redacted, stability, difficulty)
|
|
274
278
|
)
|
|
275
279
|
db.commit()
|
|
276
280
|
return cur.lastrowid
|
|
@@ -386,6 +390,12 @@ def _refine_memory(match_info: dict, new_content: str) -> int:
|
|
|
386
390
|
db = _get_db()
|
|
387
391
|
table = "stm_memories" if match_info["store"] == "stm" else "ltm_memories"
|
|
388
392
|
memory_id = match_info["id"]
|
|
393
|
+
profile_row = db.execute(
|
|
394
|
+
f"SELECT stability, difficulty FROM {table} WHERE id = ?",
|
|
395
|
+
(memory_id,),
|
|
396
|
+
).fetchone()
|
|
397
|
+
current_stability = profile_row["stability"] if profile_row else 1.0
|
|
398
|
+
current_difficulty = profile_row["difficulty"] if profile_row else 0.5
|
|
389
399
|
|
|
390
400
|
# Check word-level diff to avoid appending near-identical text
|
|
391
401
|
existing_words = set(match_info["content"].lower().split())
|
|
@@ -395,10 +405,16 @@ def _refine_memory(match_info: dict, new_content: str) -> int:
|
|
|
395
405
|
if len(unique_new) < 3:
|
|
396
406
|
# Almost no new words -- just strengthen the existing memory
|
|
397
407
|
now = datetime.utcnow().isoformat()
|
|
408
|
+
new_stability, new_difficulty = rehearsal_profile_update(
|
|
409
|
+
current_stability,
|
|
410
|
+
current_difficulty,
|
|
411
|
+
score=0.7,
|
|
412
|
+
refinement=True,
|
|
413
|
+
)
|
|
398
414
|
db.execute(
|
|
399
415
|
f"UPDATE {table} SET strength = MIN(1.0, strength + 0.1), "
|
|
400
|
-
f"access_count = access_count + 1, last_accessed = ? WHERE id = ?",
|
|
401
|
-
(now, memory_id)
|
|
416
|
+
f"access_count = access_count + 1, last_accessed = ?, stability = ?, difficulty = ? WHERE id = ?",
|
|
417
|
+
(now, new_stability, new_difficulty, memory_id)
|
|
402
418
|
)
|
|
403
419
|
db.commit()
|
|
404
420
|
return memory_id
|
|
@@ -408,11 +424,17 @@ def _refine_memory(match_info: dict, new_content: str) -> int:
|
|
|
408
424
|
new_vec = embed(merged_content)
|
|
409
425
|
new_blob = _array_to_blob(new_vec)
|
|
410
426
|
now = datetime.utcnow().isoformat()
|
|
427
|
+
new_stability, new_difficulty = rehearsal_profile_update(
|
|
428
|
+
current_stability,
|
|
429
|
+
current_difficulty,
|
|
430
|
+
score=0.82,
|
|
431
|
+
refinement=True,
|
|
432
|
+
)
|
|
411
433
|
|
|
412
434
|
db.execute(
|
|
413
435
|
f"UPDATE {table} SET content = ?, embedding = ?, strength = MIN(1.0, strength + 0.15), "
|
|
414
|
-
f"access_count = access_count + 1, last_accessed = ? WHERE id = ?",
|
|
415
|
-
(merged_content, new_blob, now, memory_id)
|
|
436
|
+
f"access_count = access_count + 1, last_accessed = ?, stability = ?, difficulty = ? WHERE id = ?",
|
|
437
|
+
(merged_content, new_blob, now, new_stability, new_difficulty, memory_id)
|
|
416
438
|
)
|
|
417
439
|
db.commit()
|
|
418
440
|
return memory_id
|
|
@@ -614,10 +636,10 @@ def process_quarantine() -> dict:
|
|
|
614
636
|
if should_promote:
|
|
615
637
|
# Promote to STM
|
|
616
638
|
cur = db.execute(
|
|
617
|
-
"""INSERT INTO stm_memories (content, embedding, source_type, source_id, source_title, domain, redaction_applied)
|
|
618
|
-
VALUES (?, ?, ?, ?, ?, ?, 0)""",
|
|
639
|
+
"""INSERT INTO stm_memories (content, embedding, source_type, source_id, source_title, domain, redaction_applied, stability, difficulty)
|
|
640
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)""",
|
|
619
641
|
(content, row["embedding"], row["source_type"], row["source_id"],
|
|
620
|
-
row["source_title"], row["domain"])
|
|
642
|
+
row["source_title"], row["domain"], *initial_memory_profile(row["source_type"], store="stm"))
|
|
621
643
|
)
|
|
622
644
|
db.execute(
|
|
623
645
|
"UPDATE quarantine SET status = 'promoted', promoted_at = datetime('now'), confidence = 1.0 WHERE id = ?",
|
|
@@ -689,10 +711,10 @@ def quarantine_promote(quarantine_id: int) -> str:
|
|
|
689
711
|
|
|
690
712
|
# Insert into STM
|
|
691
713
|
db.execute(
|
|
692
|
-
"""INSERT INTO stm_memories (content, embedding, source_type, source_id, source_title, domain, redaction_applied)
|
|
693
|
-
VALUES (?, ?, ?, ?, ?, ?, 0)""",
|
|
714
|
+
"""INSERT INTO stm_memories (content, embedding, source_type, source_id, source_title, domain, redaction_applied, stability, difficulty)
|
|
715
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)""",
|
|
694
716
|
(row["content"], row["embedding"], row["source_type"], row["source_id"],
|
|
695
|
-
row["source_title"], row["domain"])
|
|
717
|
+
row["source_title"], row["domain"], *initial_memory_profile(row["source_type"], store="stm"))
|
|
696
718
|
)
|
|
697
719
|
db.execute(
|
|
698
720
|
"UPDATE quarantine SET status = 'promoted', promoted_at = datetime('now'), confidence = 1.0 WHERE id = ?",
|
package/src/cognitive/_memory.py
CHANGED
|
@@ -406,6 +406,10 @@ def get_stats() -> dict:
|
|
|
406
406
|
|
|
407
407
|
avg_stm = db.execute("SELECT AVG(strength) FROM stm_memories WHERE lifecycle_state IN ('active', 'pinned') AND promoted_to_ltm = 0").fetchone()[0] or 0.0
|
|
408
408
|
avg_ltm = db.execute("SELECT AVG(strength) FROM ltm_memories WHERE is_dormant = 0").fetchone()[0] or 0.0
|
|
409
|
+
avg_stm_stability = db.execute("SELECT AVG(stability) FROM stm_memories WHERE lifecycle_state IN ('active', 'pinned') AND promoted_to_ltm = 0").fetchone()[0] or 0.0
|
|
410
|
+
avg_ltm_stability = db.execute("SELECT AVG(stability) FROM ltm_memories WHERE is_dormant = 0").fetchone()[0] or 0.0
|
|
411
|
+
avg_stm_difficulty = db.execute("SELECT AVG(difficulty) FROM stm_memories WHERE lifecycle_state IN ('active', 'pinned') AND promoted_to_ltm = 0").fetchone()[0] or 0.0
|
|
412
|
+
avg_ltm_difficulty = db.execute("SELECT AVG(difficulty) FROM ltm_memories WHERE is_dormant = 0").fetchone()[0] or 0.0
|
|
409
413
|
|
|
410
414
|
total_retrievals = db.execute("SELECT COUNT(*) FROM retrieval_log").fetchone()[0]
|
|
411
415
|
avg_retrieval_score = db.execute("SELECT AVG(top_score) FROM retrieval_log").fetchone()[0] or 0.0
|
|
@@ -428,6 +432,10 @@ def get_stats() -> dict:
|
|
|
428
432
|
"ltm_dormant": ltm_dormant,
|
|
429
433
|
"avg_stm_strength": round(avg_stm, 3),
|
|
430
434
|
"avg_ltm_strength": round(avg_ltm, 3),
|
|
435
|
+
"avg_stm_stability": round(avg_stm_stability, 3),
|
|
436
|
+
"avg_ltm_stability": round(avg_ltm_stability, 3),
|
|
437
|
+
"avg_stm_difficulty": round(avg_stm_difficulty, 3),
|
|
438
|
+
"avg_ltm_difficulty": round(avg_ltm_difficulty, 3),
|
|
431
439
|
"total_retrievals": total_retrievals,
|
|
432
440
|
"avg_retrieval_score": round(avg_retrieval_score, 3),
|
|
433
441
|
"top_domains_stm": [(r["domain"], r["cnt"]) for r in top_domains_stm],
|
package/src/cognitive/_search.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"""NEXO Cognitive — Search, retrieval, ranking."""
|
|
2
2
|
import math
|
|
3
|
+
import re
|
|
3
4
|
import sqlite3
|
|
4
5
|
import numpy as np
|
|
5
6
|
from datetime import datetime
|
|
6
|
-
from cognitive._core import
|
|
7
|
+
from cognitive._core import (
|
|
8
|
+
_get_db, embed, cosine_similarity, _blob_to_array, _array_to_blob,
|
|
9
|
+
_get_model, _get_reranker, rerank_results, EMBEDDING_DIM,
|
|
10
|
+
rehearsal_profile_update,
|
|
11
|
+
)
|
|
7
12
|
|
|
8
13
|
def bm25_search(query_text: str, stores: str = "both", top_k: int = 20,
|
|
9
14
|
source_type_filter: str = "") -> list[dict]:
|
|
@@ -151,6 +156,10 @@ _HISTORICAL_CUES = frozenset({
|
|
|
151
156
|
"cuando", "hace", "meses", "año", "anterior", "antes",
|
|
152
157
|
})
|
|
153
158
|
|
|
159
|
+
_EXACT_LOOKUP_RE = re.compile(
|
|
160
|
+
r"(/|\\|::|\.[A-Za-z0-9]+|#L\d+|line \d+|error[: ]|exception|traceback|0x[0-9a-fA-F]+|[A-Z]{2,}-\d+)"
|
|
161
|
+
)
|
|
162
|
+
|
|
154
163
|
|
|
155
164
|
def _apply_temporal_boost(results: list[dict], query_text: str) -> list[dict]:
|
|
156
165
|
"""Apply bounded temporal boost to retrieval results.
|
|
@@ -209,6 +218,42 @@ def _apply_temporal_boost(results: list[dict], query_text: str) -> list[dict]:
|
|
|
209
218
|
return results
|
|
210
219
|
|
|
211
220
|
|
|
221
|
+
def _looks_like_exact_lookup(query_text: str) -> bool:
|
|
222
|
+
query = str(query_text or "").strip()
|
|
223
|
+
if not query:
|
|
224
|
+
return False
|
|
225
|
+
lowered = query.lower()
|
|
226
|
+
if _EXACT_LOOKUP_RE.search(query):
|
|
227
|
+
return True
|
|
228
|
+
if any(token in lowered for token in ("file ", "path ", "port ", "localhost", "grep ", "rg ", "exact", "literal")):
|
|
229
|
+
return True
|
|
230
|
+
if any(ch in query for ch in ('"', "'", "`", "=", "[", "]")):
|
|
231
|
+
return True
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _auto_use_hyde(query_text: str, source_type_filter: str = "") -> bool:
|
|
236
|
+
query = str(query_text or "").strip()
|
|
237
|
+
if not query or source_type_filter:
|
|
238
|
+
return False
|
|
239
|
+
if len(query) < 18 or _looks_like_exact_lookup(query):
|
|
240
|
+
return False
|
|
241
|
+
intent = _classify_query_intent(query)
|
|
242
|
+
return intent in {"howto", "definition", "reasoning"} or (intent == "lookup" and len(query.split()) >= 4)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _auto_spreading_depth(query_text: str, source_type_filter: str = "") -> int:
|
|
246
|
+
query = str(query_text or "").strip().lower()
|
|
247
|
+
if not query or source_type_filter or _looks_like_exact_lookup(query):
|
|
248
|
+
return 0
|
|
249
|
+
intent = _classify_query_intent(query)
|
|
250
|
+
if intent in {"howto", "definition", "reasoning"}:
|
|
251
|
+
return 1
|
|
252
|
+
if any(connector in query for connector in (" and ", " because ", " related ", " connect ", " why ", " how ")):
|
|
253
|
+
return 1
|
|
254
|
+
return 0
|
|
255
|
+
|
|
256
|
+
|
|
212
257
|
# ============================================================================
|
|
213
258
|
# FEATURE 0.5: Knowledge Graph Boost
|
|
214
259
|
# Memories connected to more KG nodes (files, areas, other learnings) are
|
|
@@ -614,9 +659,18 @@ def _rehearse_results(results: list[dict], skip_ids: set = None):
|
|
|
614
659
|
if (r["store"], r["id"]) in skip:
|
|
615
660
|
continue
|
|
616
661
|
table = "stm_memories" if r["store"] == "stm" else "ltm_memories"
|
|
662
|
+
current = db.execute(
|
|
663
|
+
f"SELECT stability, difficulty FROM {table} WHERE id = ?",
|
|
664
|
+
(r["id"],),
|
|
665
|
+
).fetchone()
|
|
666
|
+
new_stability, new_difficulty = rehearsal_profile_update(
|
|
667
|
+
current["stability"] if current else 1.0,
|
|
668
|
+
current["difficulty"] if current else 0.5,
|
|
669
|
+
score=r.get("score", 0.6),
|
|
670
|
+
)
|
|
617
671
|
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"])
|
|
672
|
+
f"UPDATE {table} SET strength = MIN(1.0, strength + 0.08), access_count = access_count + 1, last_accessed = ?, stability = ?, difficulty = ? WHERE id = ?",
|
|
673
|
+
(now, new_stability, new_difficulty, r["id"])
|
|
620
674
|
)
|
|
621
675
|
db.commit()
|
|
622
676
|
|
|
@@ -630,18 +684,18 @@ def search(
|
|
|
630
684
|
rehearse: bool = True,
|
|
631
685
|
source_type_filter: str = "",
|
|
632
686
|
include_archived: bool = False,
|
|
633
|
-
use_hyde: bool =
|
|
687
|
+
use_hyde: bool | None = None,
|
|
634
688
|
hybrid: bool = True,
|
|
635
689
|
hybrid_alpha: float = 0.6,
|
|
636
|
-
spreading_depth: int =
|
|
690
|
+
spreading_depth: int | None = None,
|
|
637
691
|
decompose: bool = True,
|
|
638
692
|
exclude_dreams: bool = True,
|
|
639
693
|
) -> list[dict]:
|
|
640
694
|
"""Full vector search across STM and/or LTM with rehearsal and dormant reactivation.
|
|
641
695
|
|
|
642
696
|
Args:
|
|
643
|
-
use_hyde: If True,
|
|
644
|
-
spreading_depth: If >0, fetch co-activated neighbors and boost their scores
|
|
697
|
+
use_hyde: If True, force HyDE on; if False, force it off; if None, auto-enable for conceptual queries.
|
|
698
|
+
spreading_depth: If >0, fetch co-activated neighbors and boost their scores. If None, use a shallow auto-default for multi-hop queries.
|
|
645
699
|
exclude_dreams: If True (default), exclude dream_insight memories from results.
|
|
646
700
|
Dream insights are 21% of LTM and dilute search precision.
|
|
647
701
|
Set to False only when explicitly looking for cross-domain patterns.
|
|
@@ -677,6 +731,8 @@ def search(
|
|
|
677
731
|
return merged
|
|
678
732
|
|
|
679
733
|
db = _get_db()
|
|
734
|
+
resolved_use_hyde = _auto_use_hyde(query_text, source_type_filter) if use_hyde is None else bool(use_hyde)
|
|
735
|
+
resolved_spreading_depth = _auto_spreading_depth(query_text, source_type_filter) if spreading_depth is None else max(0, int(spreading_depth))
|
|
680
736
|
|
|
681
737
|
# Detect temporal queries — boost results with temporal_date
|
|
682
738
|
_temporal_keywords = {"when", "date", "time", "first", "last", "before", "after",
|
|
@@ -684,7 +740,7 @@ def search(
|
|
|
684
740
|
query_lower = query_text.lower().split()
|
|
685
741
|
is_temporal_query = bool(_temporal_keywords & set(query_lower))
|
|
686
742
|
|
|
687
|
-
if
|
|
743
|
+
if resolved_use_hyde:
|
|
688
744
|
query_vec = hyde_expand_query(query_text)
|
|
689
745
|
else:
|
|
690
746
|
query_vec = embed(query_text)
|
|
@@ -855,9 +911,9 @@ def search(
|
|
|
855
911
|
|
|
856
912
|
# Spreading activation: boost co-activated neighbors (Feature 2)
|
|
857
913
|
co_activation_applied = False
|
|
858
|
-
if
|
|
914
|
+
if resolved_spreading_depth > 0 and results:
|
|
859
915
|
memory_ids = [(r["store"], r["id"]) for r in results]
|
|
860
|
-
neighbor_boosts = _get_co_activated_neighbors(memory_ids, depth=
|
|
916
|
+
neighbor_boosts = _get_co_activated_neighbors(memory_ids, depth=resolved_spreading_depth)
|
|
861
917
|
|
|
862
918
|
if neighbor_boosts:
|
|
863
919
|
co_activation_applied = True
|
|
@@ -911,7 +967,7 @@ def search(
|
|
|
911
967
|
reactivated = r.get("reactivated", False)
|
|
912
968
|
|
|
913
969
|
ranking_desc = "semantic_similarity"
|
|
914
|
-
if
|
|
970
|
+
if resolved_use_hyde:
|
|
915
971
|
ranking_desc = "hyde_centroid_similarity"
|
|
916
972
|
parts = [f"Ranked #{rank}: {ranking_desc}={score:.3f}"]
|
|
917
973
|
parts.append(f"store={store}, strength={strength:.2f}, accesses={access_count}")
|
|
@@ -919,6 +975,10 @@ def search(
|
|
|
919
975
|
parts.append(f"kg_boost=+{r['kg_boost']:.3f} ({r.get('kg_connections', 0)} edges)")
|
|
920
976
|
if r.get("co_activation_boost"):
|
|
921
977
|
parts.append(f"co_activation_boost=+{r['co_activation_boost']:.3f}")
|
|
978
|
+
if use_hyde is None and resolved_use_hyde:
|
|
979
|
+
parts.append("hyde=auto")
|
|
980
|
+
if spreading_depth is None and resolved_spreading_depth > 0:
|
|
981
|
+
parts.append(f"spreading=auto:{resolved_spreading_depth}")
|
|
922
982
|
if created:
|
|
923
983
|
parts.append(f"created={created[:10]}")
|
|
924
984
|
if tags:
|
package/src/dashboard/app.py
CHANGED
|
@@ -28,6 +28,8 @@ _PARENT = str(Path(__file__).resolve().parent.parent)
|
|
|
28
28
|
if _PARENT not in sys.path:
|
|
29
29
|
sys.path.insert(0, _PARENT)
|
|
30
30
|
|
|
31
|
+
from agent_runner import AgentRunnerError, build_followup_terminal_shell_command
|
|
32
|
+
|
|
31
33
|
app = FastAPI(title="NEXO Brain Dashboard", version="3.0.0")
|
|
32
34
|
|
|
33
35
|
TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
|
|
@@ -137,7 +139,7 @@ class ChatMessage(BaseModel):
|
|
|
137
139
|
|
|
138
140
|
def _cognitive_db():
|
|
139
141
|
"""Direct connection to cognitive.db."""
|
|
140
|
-
nexo_home = os.environ.get("NEXO_HOME", str(Path.home() / "
|
|
142
|
+
nexo_home = os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))
|
|
141
143
|
db_path = Path(nexo_home) / "data" / "cognitive.db"
|
|
142
144
|
conn = sqlite3.connect(str(db_path))
|
|
143
145
|
conn.row_factory = sqlite3.Row
|
|
@@ -145,7 +147,8 @@ def _cognitive_db():
|
|
|
145
147
|
|
|
146
148
|
def _email_db():
|
|
147
149
|
"""Direct connection to nexo-email.db."""
|
|
148
|
-
|
|
150
|
+
nexo_home = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
151
|
+
db_path = nexo_home / "nexo-email" / "nexo-email.db"
|
|
149
152
|
if not db_path.exists():
|
|
150
153
|
return None
|
|
151
154
|
conn = sqlite3.connect(str(db_path))
|
|
@@ -420,7 +423,7 @@ async def api_sessions(limit: int = Query(10, ge=1, le=50)):
|
|
|
420
423
|
conn = db.get_db()
|
|
421
424
|
# Active sessions (from sessions table, not diaries)
|
|
422
425
|
active_rows = conn.execute(
|
|
423
|
-
"SELECT sid as session_id, task, last_update_epoch, claude_session_id "
|
|
426
|
+
"SELECT sid as session_id, task, last_update_epoch, claude_session_id, external_session_id, session_client "
|
|
424
427
|
"FROM sessions WHERE last_update_epoch > (strftime('%s','now') - 900) "
|
|
425
428
|
"ORDER BY last_update_epoch DESC"
|
|
426
429
|
).fetchall()
|
|
@@ -716,7 +719,7 @@ async def api_ops_move(body: MoveRequest):
|
|
|
716
719
|
|
|
717
720
|
@app.post("/api/ops/execute/{fid}")
|
|
718
721
|
async def api_ops_execute(fid: str):
|
|
719
|
-
"""Execute a followup by opening Terminal with
|
|
722
|
+
"""Execute a followup by opening Terminal with the configured NEXO client."""
|
|
720
723
|
db = _db()
|
|
721
724
|
conn = db.get_db()
|
|
722
725
|
row = conn.execute("SELECT * FROM followups WHERE id = ?", (fid,)).fetchone()
|
|
@@ -734,9 +737,13 @@ async def api_ops_execute(fid: str):
|
|
|
734
737
|
tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".txt", prefix="nexo-followup-", delete=False)
|
|
735
738
|
tmp.write(fid)
|
|
736
739
|
tmp.close()
|
|
737
|
-
# The
|
|
738
|
-
|
|
739
|
-
|
|
740
|
+
# The selected terminal client reads the followup ID from the temp file — no shell interpolation of description
|
|
741
|
+
try:
|
|
742
|
+
_, shell_cmd = build_followup_terminal_shell_command(tmp.name)
|
|
743
|
+
except AgentRunnerError as exc:
|
|
744
|
+
return JSONResponse({"error": str(exc)}, status_code=503)
|
|
745
|
+
escaped = shell_cmd.replace("\\", "\\\\").replace('"', '\\"')
|
|
746
|
+
script = f'tell application "Terminal" to do script "{escaped}"'
|
|
740
747
|
subprocess.Popen(["osascript", "-e", script])
|
|
741
748
|
return {"success": True, "followup_id": fid}
|
|
742
749
|
|
|
@@ -1388,7 +1395,7 @@ async def api_credentials():
|
|
|
1388
1395
|
|
|
1389
1396
|
@app.get("/api/backups")
|
|
1390
1397
|
async def api_backups():
|
|
1391
|
-
nexo_home = Path(os.environ.get("NEXO_HOME", str(Path.home() / "
|
|
1398
|
+
nexo_home = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
1392
1399
|
backup_dir = nexo_home / "backups"
|
|
1393
1400
|
data_dir = nexo_home / "data"
|
|
1394
1401
|
backups = []
|
package/src/db/_schema.py
CHANGED
|
@@ -256,6 +256,15 @@ def _m13_claude_session_id(conn):
|
|
|
256
256
|
conn.commit()
|
|
257
257
|
|
|
258
258
|
|
|
259
|
+
def _m21_external_session_fields(conn):
|
|
260
|
+
"""Generalize session linkage beyond Claude-specific naming."""
|
|
261
|
+
_migrate_add_column(conn, "sessions", "external_session_id", "TEXT DEFAULT ''")
|
|
262
|
+
_migrate_add_column(conn, "sessions", "session_client", "TEXT DEFAULT ''")
|
|
263
|
+
_migrate_add_index(conn, "idx_sessions_external_sid", "sessions", "external_session_id")
|
|
264
|
+
_migrate_add_index(conn, "idx_sessions_client", "sessions", "session_client")
|
|
265
|
+
conn.commit()
|
|
266
|
+
|
|
267
|
+
|
|
259
268
|
def _m14_learnings_priority_weight(conn):
|
|
260
269
|
"""Add priority, weight, and guard usage tracking to learnings + followup priority."""
|
|
261
270
|
_migrate_add_column(conn, "learnings", "priority", "TEXT DEFAULT 'medium'")
|
|
@@ -445,6 +454,7 @@ MIGRATIONS = [
|
|
|
445
454
|
(18, "skills_steps_column", _m18_skills_steps),
|
|
446
455
|
(19, "skills_v2", _m19_skills_v2),
|
|
447
456
|
(20, "personal_scripts_registry", _m20_personal_scripts_registry),
|
|
457
|
+
(21, "external_session_fields", _m21_external_session_fields),
|
|
448
458
|
]
|
|
449
459
|
|
|
450
460
|
|
package/src/db/_sessions.py
CHANGED
|
@@ -34,18 +34,26 @@ def _validate_sid(sid: str) -> str:
|
|
|
34
34
|
raise ValueError(f"Invalid SID format: {sid[:80]}")
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
def register_session(
|
|
37
|
+
def register_session(
|
|
38
|
+
sid: str,
|
|
39
|
+
task: str,
|
|
40
|
+
claude_session_id: str = "",
|
|
41
|
+
*,
|
|
42
|
+
external_session_id: str = "",
|
|
43
|
+
session_client: str = "",
|
|
44
|
+
) -> dict:
|
|
38
45
|
"""Register or re-register a session."""
|
|
39
46
|
sid = _validate_sid(sid)
|
|
40
47
|
conn = get_db()
|
|
41
48
|
now = now_epoch()
|
|
49
|
+
linked_session_id = (external_session_id or claude_session_id or "").strip()
|
|
42
50
|
conn.execute(
|
|
43
|
-
"INSERT OR REPLACE INTO sessions (sid, task, started_epoch, last_update_epoch, local_time, claude_session_id) "
|
|
44
|
-
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
45
|
-
(sid, task, now, now, local_time_str(),
|
|
51
|
+
"INSERT OR REPLACE INTO sessions (sid, task, started_epoch, last_update_epoch, local_time, claude_session_id, external_session_id, session_client) "
|
|
52
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
53
|
+
(sid, task, now, now, local_time_str(), linked_session_id, linked_session_id, (session_client or "").strip())
|
|
46
54
|
)
|
|
47
55
|
conn.commit()
|
|
48
|
-
return {"sid": sid, "task": task}
|
|
56
|
+
return {"sid": sid, "task": task, "external_session_id": linked_session_id, "session_client": (session_client or "").strip()}
|
|
49
57
|
|
|
50
58
|
|
|
51
59
|
def update_session(sid: str, task: str | None) -> dict:
|
|
@@ -320,4 +328,3 @@ def _expire_old_questions(conn: sqlite3.Connection):
|
|
|
320
328
|
(cutoff,)
|
|
321
329
|
)
|
|
322
330
|
|
|
323
|
-
|
|
@@ -9,6 +9,7 @@ import plistlib
|
|
|
9
9
|
import subprocess
|
|
10
10
|
import sys
|
|
11
11
|
import time
|
|
12
|
+
import tomllib
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
14
15
|
from client_preferences import (
|
|
@@ -39,6 +40,32 @@ OPTIONALS_FILE = NEXO_HOME / "config" / "optionals.json"
|
|
|
39
40
|
SCHEDULE_FILE = NEXO_HOME / "config" / "schedule.json"
|
|
40
41
|
|
|
41
42
|
|
|
43
|
+
def _codex_bootstrap_config_status() -> dict:
|
|
44
|
+
path = Path.home() / ".codex" / "config.toml"
|
|
45
|
+
if not path.is_file():
|
|
46
|
+
return {"exists": False, "path": str(path), "bootstrap_managed": False}
|
|
47
|
+
try:
|
|
48
|
+
payload = tomllib.loads(path.read_text())
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
return {
|
|
51
|
+
"exists": True,
|
|
52
|
+
"path": str(path),
|
|
53
|
+
"bootstrap_managed": False,
|
|
54
|
+
"error": str(exc),
|
|
55
|
+
}
|
|
56
|
+
managed = bool(payload.get("nexo", {}).get("codex", {}).get("bootstrap_managed"))
|
|
57
|
+
initial_messages = payload.get("initial_messages", [])
|
|
58
|
+
has_initial_messages = bool(initial_messages)
|
|
59
|
+
return {
|
|
60
|
+
"exists": True,
|
|
61
|
+
"path": str(path),
|
|
62
|
+
"bootstrap_managed": managed,
|
|
63
|
+
"has_initial_messages": has_initial_messages,
|
|
64
|
+
"model": str(payload.get("model", "") or ""),
|
|
65
|
+
"reasoning_effort": str(payload.get("model_reasoning_effort", "") or ""),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
42
69
|
def _file_age_seconds(path: Path) -> float | None:
|
|
43
70
|
"""Return file age in seconds, or None if not found."""
|
|
44
71
|
try:
|
|
@@ -1054,13 +1081,41 @@ def check_client_bootstrap_parity(fix: bool = False) -> DoctorCheck:
|
|
|
1054
1081
|
f"`{client_key}` bootstrap version {info.get('version') or 'unknown'} != template {info.get('template_version')}"
|
|
1055
1082
|
)
|
|
1056
1083
|
repair_plan.append("Refresh bootstrap files from the current NEXO templates")
|
|
1084
|
+
if client_key == "codex":
|
|
1085
|
+
codex_config = _codex_bootstrap_config_status()
|
|
1086
|
+
if codex_config.get("error"):
|
|
1087
|
+
status = "degraded"
|
|
1088
|
+
severity = "warn"
|
|
1089
|
+
evidence.append(f"codex config TOML invalid at {codex_config.get('path')}: {codex_config.get('error')}")
|
|
1090
|
+
repair_plan.append("Repair ~/.codex/config.toml so NEXO can manage Codex bootstrap and model defaults")
|
|
1091
|
+
elif codex_config.get("exists") and not codex_config.get("bootstrap_managed"):
|
|
1092
|
+
status = "degraded"
|
|
1093
|
+
severity = "warn"
|
|
1094
|
+
evidence.append(f"codex config missing managed bootstrap injection at {codex_config.get('path')}")
|
|
1095
|
+
repair_plan.append("Run `nexo clients sync` or `nexo update` so plain Codex sessions inherit the NEXO bootstrap")
|
|
1096
|
+
elif codex_config.get("exists"):
|
|
1097
|
+
evidence.append(
|
|
1098
|
+
"codex config bootstrap managed"
|
|
1099
|
+
+ (
|
|
1100
|
+
f" ({codex_config.get('model') or 'default'}, {codex_config.get('reasoning_effort') or 'default'})"
|
|
1101
|
+
)
|
|
1102
|
+
)
|
|
1057
1103
|
|
|
1058
1104
|
if fix and status != "healthy":
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1105
|
+
try:
|
|
1106
|
+
from client_sync import sync_all_clients
|
|
1107
|
+
sync_all_clients(
|
|
1108
|
+
nexo_home=NEXO_HOME,
|
|
1109
|
+
runtime_root=NEXO_CODE,
|
|
1110
|
+
user_home=Path.home(),
|
|
1111
|
+
preferences=prefs,
|
|
1112
|
+
)
|
|
1113
|
+
except Exception:
|
|
1114
|
+
sync_enabled_bootstraps(
|
|
1115
|
+
nexo_home=NEXO_HOME,
|
|
1116
|
+
user_home=Path.home(),
|
|
1117
|
+
preferences=prefs,
|
|
1118
|
+
)
|
|
1064
1119
|
post = check_client_bootstrap_parity(fix=False)
|
|
1065
1120
|
if post.status == "healthy":
|
|
1066
1121
|
post.fixed = True
|
|
@@ -113,8 +113,8 @@ conn.row_factory = sqlite3.Row
|
|
|
113
113
|
row = None
|
|
114
114
|
if session_id and session_id != 'global':
|
|
115
115
|
row = conn.execute(
|
|
116
|
-
'SELECT sid, task FROM sessions WHERE claude_session_id = ? LIMIT 1',
|
|
117
|
-
(session_id,)
|
|
116
|
+
'SELECT sid, task FROM sessions WHERE external_session_id = ? OR claude_session_id = ? LIMIT 1',
|
|
117
|
+
(session_id, session_id)
|
|
118
118
|
).fetchone()
|
|
119
119
|
|
|
120
120
|
# Fallback: most recent active session
|
package/src/hooks/inbox-hook.sh
CHANGED
|
@@ -32,7 +32,7 @@ DB="$NEXO_HOME/data/nexo.db"
|
|
|
32
32
|
mkdir -p "$NEXO_HOME/data"
|
|
33
33
|
[ -f "$DB" ] || exit 0
|
|
34
34
|
|
|
35
|
-
NEXO_SID=$(sqlite3 "$DB" "SELECT sid FROM sessions WHERE claude_session_id = '${CLAUDE_SID}' AND last_update_epoch > (strftime('%s','now') - 900) ORDER BY last_update_epoch DESC LIMIT 1;" 2>/dev/null)
|
|
35
|
+
NEXO_SID=$(sqlite3 "$DB" "SELECT sid FROM sessions WHERE (external_session_id = '${CLAUDE_SID}' OR claude_session_id = '${CLAUDE_SID}') AND last_update_epoch > (strftime('%s','now') - 900) ORDER BY last_update_epoch DESC LIMIT 1;" 2>/dev/null)
|
|
36
36
|
[ -z "$NEXO_SID" ] && exit 0
|
|
37
37
|
|
|
38
38
|
# 5. Check inbox — messages addressed to this session or broadcast
|
|
@@ -19,8 +19,8 @@ def handle_cognitive_retrieve(
|
|
|
19
19
|
source_type: str = "",
|
|
20
20
|
domain: str = "",
|
|
21
21
|
include_archived: bool = False,
|
|
22
|
-
use_hyde: bool =
|
|
23
|
-
spreading_depth: int =
|
|
22
|
+
use_hyde: bool | None = None,
|
|
23
|
+
spreading_depth: int | None = None,
|
|
24
24
|
) -> str:
|
|
25
25
|
"""RAG query over cognitive memory (STM + LTM). Triggers rehearsal on retrieved memories.
|
|
26
26
|
|
|
@@ -32,8 +32,8 @@ def handle_cognitive_retrieve(
|
|
|
32
32
|
source_type: Filter by source type e.g. "change", "learning", "diary" (default: all)
|
|
33
33
|
domain: Filter by domain e.g. "project-a", "shopify" (default: all)
|
|
34
34
|
include_archived: If True, also search archived memories (default False)
|
|
35
|
-
use_hyde: If True,
|
|
36
|
-
spreading_depth: If >0, boost co-activated neighbors
|
|
35
|
+
use_hyde: If True/False, force HyDE on/off. If omitted, NEXO auto-enables it for conceptual queries.
|
|
36
|
+
spreading_depth: If >0, boost co-activated neighbors directly. If omitted, NEXO may auto-enable shallow spreading for multi-hop queries.
|
|
37
37
|
"""
|
|
38
38
|
if not query or not query.strip():
|
|
39
39
|
return "ERROR: query is required."
|
|
@@ -57,10 +57,14 @@ def handle_cognitive_retrieve(
|
|
|
57
57
|
|
|
58
58
|
formatted = cognitive.format_results(results)
|
|
59
59
|
mode_parts = [f"stores={stores}", f"min_score={min_score}"]
|
|
60
|
-
if use_hyde:
|
|
60
|
+
if use_hyde is True:
|
|
61
61
|
mode_parts.append("hyde=ON")
|
|
62
|
-
|
|
62
|
+
elif use_hyde is None:
|
|
63
|
+
mode_parts.append("hyde=AUTO")
|
|
64
|
+
if spreading_depth and spreading_depth > 0:
|
|
63
65
|
mode_parts.append(f"spreading={spreading_depth}")
|
|
66
|
+
elif spreading_depth is None:
|
|
67
|
+
mode_parts.append("spreading=AUTO")
|
|
64
68
|
header = f"COGNITIVE RETRIEVE — query: '{query}' | {len(results)} results ({', '.join(mode_parts)})\n\n"
|
|
65
69
|
return header + formatted
|
|
66
70
|
|
|
@@ -76,6 +80,10 @@ def handle_cognitive_stats() -> str:
|
|
|
76
80
|
f" LTM dormant: {stats['ltm_dormant']}",
|
|
77
81
|
f" Avg STM strength: {stats['avg_stm_strength']:.3f}",
|
|
78
82
|
f" Avg LTM strength: {stats['avg_ltm_strength']:.3f}",
|
|
83
|
+
f" Avg STM stability: {stats.get('avg_stm_stability', 0.0):.3f}",
|
|
84
|
+
f" Avg LTM stability: {stats.get('avg_ltm_stability', 0.0):.3f}",
|
|
85
|
+
f" Avg STM difficulty: {stats.get('avg_stm_difficulty', 0.0):.3f}",
|
|
86
|
+
f" Avg LTM difficulty: {stats.get('avg_ltm_difficulty', 0.0):.3f}",
|
|
79
87
|
f" Total retrievals: {stats['total_retrievals']}",
|
|
80
88
|
f" Avg retrieval score: {stats['avg_retrieval_score']:.3f}",
|
|
81
89
|
]
|