nexo-brain 5.3.19 → 5.3.21
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/bin/nexo-brain.js +52 -10
- package/package.json +1 -1
- package/src/auto_update.py +11 -8
- package/src/dashboard/static/favicon 2.svg +32 -0
- package/src/dashboard/static/nexo-logo 2.png +0 -0
- package/src/dashboard/static/nexo-logo 2.svg +40 -0
- package/src/dashboard/static/style 2.css +2458 -0
- package/src/dashboard/templates/adaptive 2.html +118 -0
- package/src/dashboard/templates/artifacts 2.html +133 -0
- package/src/dashboard/templates/backups 2.html +136 -0
- package/src/dashboard/templates/base 2.html +417 -0
- package/src/dashboard/templates/calendar 2.html +591 -0
- package/src/dashboard/templates/chat 2.html +356 -0
- package/src/dashboard/templates/claims 2.html +259 -0
- package/src/dashboard/templates/cortex 2.html +321 -0
- package/src/dashboard/templates/credentials 2.html +128 -0
- package/src/dashboard/templates/crons 2.html +370 -0
- package/src/dashboard/templates/dashboard 2.html +494 -0
- package/src/dashboard/templates/dreams 2.html +252 -0
- package/src/dashboard/templates/email 2.html +160 -0
- package/src/dashboard/templates/evolution 2.html +189 -0
- package/src/dashboard/templates/feed 2.html +249 -0
- package/src/dashboard/templates/followup_health 2.html +170 -0
- package/src/dashboard/templates/graph 2.html +201 -0
- package/src/dashboard/templates/guard 2.html +259 -0
- package/src/dashboard/templates/inbox 2.html +251 -0
- package/src/dashboard/templates/memory 2.html +420 -0
- package/src/dashboard/templates/operations 2.html +608 -0
- package/src/dashboard/templates/plugins 2.html +185 -0
- package/src/dashboard/templates/protocol 2.html +199 -0
- package/src/dashboard/templates/rules 2.html +246 -0
- package/src/dashboard/templates/sentiment 2.html +247 -0
- package/src/dashboard/templates/sessions 2.html +218 -0
- package/src/dashboard/templates/skills 2.html +329 -0
- package/src/dashboard/templates/somatic 2.html +73 -0
- package/src/dashboard/templates/triggers 2.html +133 -0
- package/src/dashboard/templates/trust 2.html +360 -0
- package/src/db/__init__ 2.py +259 -0
- package/src/db/_core 2.py +437 -0
- package/src/db/_credentials 2.py +124 -0
- package/src/db/_episodic 2.py +762 -0
- package/src/db/_evolution 2.py +54 -0
- package/src/db/_fts 2.py +406 -0
- package/src/db/_goal_profiles 2.py +376 -0
- package/src/db/_hot_context 2.py +660 -0
- package/src/db/_outcomes 2.py +800 -0
- package/src/db/_personal_scripts 2.py +582 -0
- package/src/db/_sessions 2.py +330 -0
- package/src/db/_tasks 2.py +91 -0
- package/src/db/_watchers 2.py +173 -0
- package/src/doctor/formatters 2.py +52 -0
- package/src/doctor/models 2.py +69 -0
- package/src/doctor/planes 2.py +87 -0
- package/src/doctor/providers/__init__ 2.py +1 -0
- package/src/doctor/providers/deep 2.py +367 -0
- package/src/evolution_cycle 2.py +519 -0
- package/src/hooks/auto_capture 2.py +208 -0
- package/src/hooks/caffeinate-guard 2.sh +8 -0
- package/src/hooks/capture-session 2.sh +21 -0
- package/src/hooks/capture-tool-logs 2.sh +158 -0
- package/src/hooks/daily-briefing-check 2.sh +33 -0
- package/src/hooks/heartbeat-enforcement 2.py +90 -0
- package/src/hooks/heartbeat-posttool 2.sh +18 -0
- package/src/hooks/inbox-hook 2.sh +76 -0
- package/src/hooks/post-compact 2.sh +152 -0
- package/src/hooks/pre-compact 2.sh +169 -0
- package/src/hooks/protocol-guardrail 2.sh +10 -0
- package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
- package/src/hooks/session-stop 2.sh +52 -0
- package/src/kg_populate 2.py +292 -0
- package/src/maintenance 2.py +53 -0
- package/src/memory_backends 2.py +71 -0
- package/src/migrate_embeddings 2.py +124 -0
- package/src/nexo_sdk 2.py +103 -0
- package/src/observability 2.py +199 -0
- package/src/plugin_loader 2.py +217 -0
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/artifact_registry 2.py +450 -0
- package/src/plugins/backup 2.py +127 -0
- package/src/plugins/claims_tools 2.py +119 -0
- package/src/plugins/cognitive_memory 2.py +609 -0
- package/src/plugins/core_rules 2.py +252 -0
- package/src/plugins/cortex 2.py +1155 -0
- package/src/plugins/entities 2.py +67 -0
- package/src/plugins/episodic_memory 2.py +560 -0
- package/src/plugins/evolution 2.py +167 -0
- package/src/plugins/goal_engine 2.py +142 -0
- package/src/plugins/guard 2.py +862 -0
- package/src/plugins/impact 2.py +29 -0
- package/src/plugins/knowledge_graph_tools 2.py +137 -0
- package/src/plugins/media_memory_tools 2.py +98 -0
- package/src/plugins/memory_export 2.py +196 -0
- package/src/plugins/outcomes 2.py +130 -0
- package/src/plugins/personal_scripts 2.py +117 -0
- package/src/plugins/preferences 2.py +47 -0
- package/src/plugins/protocol 2.py +1449 -0
- package/src/plugins/simple_api 2.py +106 -0
- package/src/plugins/skills 2.py +341 -0
- package/src/plugins/state_watchers 2.py +79 -0
- package/src/plugins/update 2.py +986 -0
- package/src/plugins/user_state_tools 2.py +43 -0
- package/src/plugins/workflow 2.py +588 -0
- package/src/protocol_settings 2.py +59 -0
- package/src/public_contribution 2.py +466 -0
- package/src/public_evolution_queue 2.py +241 -0
- package/src/requirements 2.txt +14 -0
- package/src/retroactive_learnings 2.py +373 -0
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +331 -0
- package/src/rules/migrate 2.py +207 -0
- package/src/runtime_power 2.py +874 -0
- package/src/script_registry 2.py +1559 -0
- package/src/scripts/check-context 2.py +272 -0
- package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
- package/src/scripts/deep-sleep/collect 2.py +928 -0
- package/src/scripts/deep-sleep/extract 2.py +330 -0
- package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
- package/src/scripts/deep-sleep/synthesize 2.py +312 -0
- package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
- package/src/scripts/nexo-agent-run 2.py +75 -0
- package/src/scripts/nexo-auto-update 2.py +6 -0
- package/src/scripts/nexo-backup 2.sh +25 -0
- package/src/scripts/nexo-brain-activation 2.sh +140 -0
- package/src/scripts/nexo-catchup 2.py +300 -0
- package/src/scripts/nexo-cognitive-decay 2.py +257 -0
- package/src/scripts/nexo-cortex-cycle 2.py +293 -0
- package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
- package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
- package/src/scripts/nexo-dashboard 2.sh +29 -0
- package/src/scripts/nexo-deep-sleep 2.sh +86 -0
- package/src/scripts/nexo-evolution-run 2.py +1664 -0
- package/src/scripts/nexo-followup-hygiene 2.py +139 -0
- package/src/scripts/nexo-hook-record 2.py +42 -0
- package/src/scripts/nexo-immune 2.py +936 -0
- package/src/scripts/nexo-impact-scorer 2.py +117 -0
- package/src/scripts/nexo-inbox-hook 2.sh +74 -0
- package/src/scripts/nexo-install 2.py +6 -0
- package/src/scripts/nexo-learning-housekeep 2.py +401 -0
- package/src/scripts/nexo-learning-validator 2.py +266 -0
- package/src/scripts/nexo-migrate 2.py +260 -0
- package/src/scripts/nexo-outcome-checker 2.py +127 -0
- package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
- package/src/scripts/nexo-pre-commit 2.py +120 -0
- package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
- package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
- package/src/scripts/nexo-reflection 2.py +256 -0
- package/src/scripts/nexo-runtime-preflight 2.py +274 -0
- package/src/scripts/nexo-sleep 2.py +631 -0
- package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
- package/src/scripts/nexo-sync-clients 2.py +16 -0
- package/src/scripts/nexo-synthesis 2.py +475 -0
- package/src/scripts/nexo-tcc-approve 2.sh +79 -0
- package/src/scripts/nexo-update 2.sh +306 -0
- package/src/scripts/nexo-watchdog 2.sh +1207 -0
- package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
- package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
- package/src/server 2.py +1296 -0
- package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
- package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
- package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
- package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
- package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
- package/src/skills/run-release-final-audit/guide 2.md +16 -0
- package/src/skills/run-release-final-audit/script 2.py +259 -0
- package/src/skills/run-release-final-audit/skill 2.json +77 -0
- package/src/skills/run-runtime-doctor/guide 2.md +12 -0
- package/src/skills/run-runtime-doctor/script 2.py +21 -0
- package/src/skills/run-runtime-doctor/skill 2.json +25 -0
- package/src/skills_runtime 2.py +932 -0
- package/src/state_watchers_runtime 2.py +475 -0
- package/src/storage_router 2.py +32 -0
- package/src/system_catalog 2.py +786 -0
- package/src/tools_coordination 2.py +103 -0
- package/src/tools_credentials 2.py +68 -0
- package/src/tools_drive 2.py +487 -0
- package/src/tools_hot_context 2.py +163 -0
- package/src/tools_learnings 2.py +612 -0
- package/src/tools_menu 2.py +229 -0
- package/src/tools_reminders 2.py +88 -0
- package/src/tools_reminders_crud 2.py +363 -0
- package/src/tools_sessions 2.py +1054 -0
- package/src/tools_system_catalog 2.py +19 -0
- package/src/tools_task_history 2.py +57 -0
- package/src/tools_transcripts 2.py +98 -0
- package/src/transcript_utils 2.py +412 -0
- package/src/user_context 2.py +46 -0
- package/src/user_data_portability 2.py +328 -0
- package/src/user_state_model 2.py +170 -0
- package/templates/CLAUDE.md 2.template +108 -0
- package/templates/CODEX.AGENTS.md 2.template +66 -0
- package/templates/launchagents/README 2.md +132 -0
- package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
- package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
- package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
- package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
- package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
- package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
- package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
- package/templates/launchagents/com.nexo.immune 2.plist +41 -0
- package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
- package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
- package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
- package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
- package/templates/nexo_helper 2.py +301 -0
- package/templates/openclaw 2.json +13 -0
- package/templates/plugin-template 2.py +40 -0
- package/templates/script-template 2.py +59 -0
- package/templates/script-template 2.sh +13 -0
- package/templates/skill-script-template 2.py +48 -0
- package/templates/skill-template 2.md +33 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Placeholder module for historical `maintenance` runner.
|
|
2
|
+
|
|
3
|
+
HISTORICAL CONTEXT (NEXO-AUDIT-2026-04-11 finding, Learning #194)
|
|
4
|
+
==================================================================
|
|
5
|
+
|
|
6
|
+
This module previously exposed `check_and_run_overdue()` and a private
|
|
7
|
+
`_run_task()` dispatcher that walked the `maintenance_schedule` table and
|
|
8
|
+
executed cognitive decay, synthesis, self audit, weight learning, somatic
|
|
9
|
+
decay, somatic projection, drive decay and graph maintenance as needed.
|
|
10
|
+
|
|
11
|
+
None of that code was ever called from anywhere. A repository-wide grep
|
|
12
|
+
for `check_and_run_overdue`, `from maintenance`, `import maintenance` and
|
|
13
|
+
`maintenance.check` produced zero hits during the 2026-04-11 audit. Each
|
|
14
|
+
of those tasks actually runs from its own LaunchAgent via its own script
|
|
15
|
+
in `src/scripts/` (e.g. `nexo-cognitive-decay.py`, `nexo-daily-self-audit
|
|
16
|
+
.py`, `nexo-evolution-run.py`), not through this dispatcher. The
|
|
17
|
+
`maintenance_schedule` table in SQLite is still populated by migrations
|
|
18
|
+
(see `db/_schema.py::_m9_maintenance_schedule` and the drive_decay
|
|
19
|
+
registration) but is effectively dead data: nothing reads it.
|
|
20
|
+
|
|
21
|
+
Why the dispatcher was removed
|
|
22
|
+
------------------------------
|
|
23
|
+
The dead dispatcher was actively misleading: developers reading the code
|
|
24
|
+
could reasonably conclude that adding a row to `maintenance_schedule`
|
|
25
|
+
would cause the named task to run. It would not. That false contract
|
|
26
|
+
was the root of a near-miss during the Item 9 fix of the 2026-04-11
|
|
27
|
+
audit, where the first plan was to register a new `read_token_cleanup`
|
|
28
|
+
task via this mechanism before discovering that the mechanism is never
|
|
29
|
+
invoked. See `src/db/_reminders.py::_purge_expired_read_tokens_if_due`
|
|
30
|
+
for the opportunistic-cleanup pattern that replaced that initial plan.
|
|
31
|
+
|
|
32
|
+
What to do if you need scheduled maintenance
|
|
33
|
+
--------------------------------------------
|
|
34
|
+
Do NOT reintroduce a dispatcher in this module. Pick one of:
|
|
35
|
+
|
|
36
|
+
* Add your work to an existing LaunchAgent script under
|
|
37
|
+
`src/scripts/` that already runs on the cadence you need.
|
|
38
|
+
* Register a new personal script via `nexo_personal_script_create` and
|
|
39
|
+
let the schedule/LaunchAgent system handle it.
|
|
40
|
+
* Run the cleanup opportunistically inside the hot path, throttled
|
|
41
|
+
by wall-clock (see `_purge_expired_read_tokens_if_due`).
|
|
42
|
+
|
|
43
|
+
What happens to the `maintenance_schedule` table
|
|
44
|
+
-------------------------------------------------
|
|
45
|
+
The table is intentionally left in place. Removing it would require a
|
|
46
|
+
destructive migration for every installed user with no benefit — the
|
|
47
|
+
rows do no harm, cost a few KB each, and their removal is deferred to a
|
|
48
|
+
future cleanup pass when migration numbering is renegotiated.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Explicit backend registry for memory expansion layers.
|
|
4
|
+
|
|
5
|
+
NEXO's historical memory system is still heavily SQLite-shaped, but newer layers
|
|
6
|
+
should not keep backend assumptions implicit forever. This module introduces a
|
|
7
|
+
small registry/contract that expansion surfaces can use today while SQLite
|
|
8
|
+
remains the default backend.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class MemoryBackendInfo:
|
|
17
|
+
key: str
|
|
18
|
+
label: str
|
|
19
|
+
description: str
|
|
20
|
+
supports: tuple[str, ...]
|
|
21
|
+
maturity: str = "stable"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_REGISTRY: dict[str, MemoryBackendInfo] = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def register_backend(info: MemoryBackendInfo) -> None:
|
|
28
|
+
_REGISTRY[info.key] = info
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def active_backend_key() -> str:
|
|
32
|
+
return (os.environ.get("NEXO_MEMORY_BACKEND", "sqlite") or "sqlite").strip().lower()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_backend(key: str = "") -> MemoryBackendInfo:
|
|
36
|
+
selected = (key or active_backend_key()).strip().lower()
|
|
37
|
+
return _REGISTRY.get(selected, _REGISTRY["sqlite"])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def list_backends() -> list[dict]:
|
|
41
|
+
active = active_backend_key()
|
|
42
|
+
results = []
|
|
43
|
+
for key in sorted(_REGISTRY):
|
|
44
|
+
info = _REGISTRY[key]
|
|
45
|
+
item = {
|
|
46
|
+
"key": info.key,
|
|
47
|
+
"label": info.label,
|
|
48
|
+
"description": info.description,
|
|
49
|
+
"supports": list(info.supports),
|
|
50
|
+
"maturity": info.maturity,
|
|
51
|
+
"active": info.key == active,
|
|
52
|
+
}
|
|
53
|
+
results.append(item)
|
|
54
|
+
return results
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
register_backend(
|
|
58
|
+
MemoryBackendInfo(
|
|
59
|
+
key="sqlite",
|
|
60
|
+
label="SQLite + FTS5",
|
|
61
|
+
description="Local-first default backend used by NEXO runtime surfaces.",
|
|
62
|
+
supports=(
|
|
63
|
+
"cognitive_core",
|
|
64
|
+
"claims",
|
|
65
|
+
"media_memory",
|
|
66
|
+
"user_state",
|
|
67
|
+
"memory_export",
|
|
68
|
+
"auto_flush",
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Migrate cognitive.db embeddings between models.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python migrate_embeddings.py upgrade # 384 → 768 (bge-small → bge-base)
|
|
7
|
+
python migrate_embeddings.py rollback # Restore from backup
|
|
8
|
+
python migrate_embeddings.py verify # Check current embedding dims
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import sqlite3
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
|
|
19
|
+
_data_dir = os.path.join(NEXO_HOME, "data")
|
|
20
|
+
os.makedirs(_data_dir, exist_ok=True)
|
|
21
|
+
DB_PATH = os.path.join(_data_dir, "cognitive.db")
|
|
22
|
+
BACKUP_PATH = DB_PATH + ".bak-384dims-pre-upgrade"
|
|
23
|
+
|
|
24
|
+
MODELS = {
|
|
25
|
+
"small": ("BAAI/bge-small-en-v1.5", 384),
|
|
26
|
+
"base": ("BAAI/bge-base-en-v1.5", 768),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def verify():
|
|
31
|
+
"""Check current embedding dimensions in the database."""
|
|
32
|
+
conn = sqlite3.connect(DB_PATH)
|
|
33
|
+
try:
|
|
34
|
+
for table in ["stm_memories", "ltm_memories"]:
|
|
35
|
+
count = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
|
|
36
|
+
if count == 0:
|
|
37
|
+
print(f" {table}: {count} rows (empty)")
|
|
38
|
+
continue
|
|
39
|
+
row = conn.execute(f"SELECT embedding FROM {table} LIMIT 1").fetchone()
|
|
40
|
+
vec = np.frombuffer(row[0], dtype=np.float32)
|
|
41
|
+
print(f" {table}: {count} rows, embedding dim = {len(vec)}")
|
|
42
|
+
finally:
|
|
43
|
+
conn.close()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def upgrade():
|
|
47
|
+
"""Re-embed all memories from bge-small (384) to bge-base (768)."""
|
|
48
|
+
from fastembed import TextEmbedding
|
|
49
|
+
|
|
50
|
+
# Verify current state
|
|
51
|
+
print("Current state:")
|
|
52
|
+
verify()
|
|
53
|
+
|
|
54
|
+
# Verify backup exists
|
|
55
|
+
if not os.path.exists(BACKUP_PATH):
|
|
56
|
+
print(f"\nCreating backup at {BACKUP_PATH}")
|
|
57
|
+
shutil.copy2(DB_PATH, BACKUP_PATH)
|
|
58
|
+
else:
|
|
59
|
+
print(f"\nBackup already exists at {BACKUP_PATH}")
|
|
60
|
+
|
|
61
|
+
# Load new model
|
|
62
|
+
model_name, expected_dim = MODELS["base"]
|
|
63
|
+
print(f"\nLoading {model_name}...")
|
|
64
|
+
model = TextEmbedding(model_name)
|
|
65
|
+
|
|
66
|
+
conn = sqlite3.connect(DB_PATH)
|
|
67
|
+
try:
|
|
68
|
+
for table in ["stm_memories", "ltm_memories"]:
|
|
69
|
+
rows = conn.execute(f"SELECT id, content FROM {table}").fetchall()
|
|
70
|
+
if not rows:
|
|
71
|
+
print(f"\n{table}: empty, skipping")
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
print(f"\n{table}: re-embedding {len(rows)} memories...")
|
|
75
|
+
t0 = time.time()
|
|
76
|
+
|
|
77
|
+
# Batch embed for speed
|
|
78
|
+
contents = [r[1] for r in rows]
|
|
79
|
+
ids = [r[0] for r in rows]
|
|
80
|
+
|
|
81
|
+
embeddings = list(model.embed(contents))
|
|
82
|
+
|
|
83
|
+
for mem_id, emb in zip(ids, embeddings):
|
|
84
|
+
blob = np.array(emb, dtype=np.float32).tobytes()
|
|
85
|
+
conn.execute(f"UPDATE {table} SET embedding = ? WHERE id = ?", (blob, mem_id))
|
|
86
|
+
|
|
87
|
+
conn.commit()
|
|
88
|
+
elapsed = time.time() - t0
|
|
89
|
+
print(f" Done: {len(rows)} memories in {elapsed:.1f}s ({elapsed/len(rows)*1000:.0f}ms/memory)")
|
|
90
|
+
finally:
|
|
91
|
+
conn.close()
|
|
92
|
+
|
|
93
|
+
print("\nAfter upgrade:")
|
|
94
|
+
verify()
|
|
95
|
+
print("\nUpgrade complete. Run 'verify' to confirm.")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def rollback():
|
|
99
|
+
"""Restore database from pre-upgrade backup."""
|
|
100
|
+
if not os.path.exists(BACKUP_PATH):
|
|
101
|
+
print(f"ERROR: Backup not found at {BACKUP_PATH}")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
print(f"Restoring from {BACKUP_PATH}...")
|
|
105
|
+
shutil.copy2(BACKUP_PATH, DB_PATH)
|
|
106
|
+
print("Restored. Current state:")
|
|
107
|
+
verify()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
if len(sys.argv) < 2:
|
|
112
|
+
print("Usage: python migrate_embeddings.py [upgrade|rollback|verify]")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
cmd = sys.argv[1]
|
|
116
|
+
if cmd == "upgrade":
|
|
117
|
+
upgrade()
|
|
118
|
+
elif cmd == "rollback":
|
|
119
|
+
rollback()
|
|
120
|
+
elif cmd == "verify":
|
|
121
|
+
verify()
|
|
122
|
+
else:
|
|
123
|
+
print(f"Unknown command: {cmd}")
|
|
124
|
+
sys.exit(1)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Minimal Python SDK for the public NEXO mental model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class NEXOClient:
|
|
12
|
+
"""Tiny Python wrapper around `nexo call` for common public operations."""
|
|
13
|
+
|
|
14
|
+
nexo_bin: str = "nexo"
|
|
15
|
+
|
|
16
|
+
def call(self, tool: str, payload: dict | None = None) -> dict | list | str:
|
|
17
|
+
result = subprocess.run(
|
|
18
|
+
[
|
|
19
|
+
self.nexo_bin,
|
|
20
|
+
"call",
|
|
21
|
+
tool,
|
|
22
|
+
"--input",
|
|
23
|
+
json.dumps(payload or {}, ensure_ascii=False),
|
|
24
|
+
"--json-output",
|
|
25
|
+
],
|
|
26
|
+
capture_output=True,
|
|
27
|
+
text=True,
|
|
28
|
+
check=False,
|
|
29
|
+
)
|
|
30
|
+
if result.returncode != 0:
|
|
31
|
+
raise RuntimeError((result.stderr or result.stdout or f"{tool} failed").strip())
|
|
32
|
+
text = (result.stdout or "").strip()
|
|
33
|
+
if not text:
|
|
34
|
+
return {}
|
|
35
|
+
try:
|
|
36
|
+
return json.loads(text)
|
|
37
|
+
except json.JSONDecodeError:
|
|
38
|
+
return {"result": text}
|
|
39
|
+
|
|
40
|
+
def remember(
|
|
41
|
+
self,
|
|
42
|
+
content: str,
|
|
43
|
+
*,
|
|
44
|
+
title: str = "",
|
|
45
|
+
domain: str = "",
|
|
46
|
+
source_type: str = "note",
|
|
47
|
+
tags: str = "",
|
|
48
|
+
bypass_gate: bool = True,
|
|
49
|
+
) -> dict | list | str:
|
|
50
|
+
return self.call(
|
|
51
|
+
"nexo_remember",
|
|
52
|
+
{
|
|
53
|
+
"content": content,
|
|
54
|
+
"title": title,
|
|
55
|
+
"domain": domain,
|
|
56
|
+
"source_type": source_type,
|
|
57
|
+
"tags": tags,
|
|
58
|
+
"bypass_gate": bypass_gate,
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def recall(self, query: str, *, days: int = 30) -> dict | list | str:
|
|
63
|
+
return self.call("nexo_memory_recall", {"query": query, "days": days})
|
|
64
|
+
|
|
65
|
+
def consolidate(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
max_insights: int = 12,
|
|
69
|
+
threshold: float = 0.9,
|
|
70
|
+
dry_run: bool = False,
|
|
71
|
+
) -> dict | list | str:
|
|
72
|
+
return self.call(
|
|
73
|
+
"nexo_consolidate",
|
|
74
|
+
{
|
|
75
|
+
"max_insights": max_insights,
|
|
76
|
+
"threshold": threshold,
|
|
77
|
+
"dry_run": dry_run,
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def run_workflow(
|
|
82
|
+
self,
|
|
83
|
+
sid: str,
|
|
84
|
+
goal: str,
|
|
85
|
+
*,
|
|
86
|
+
steps: list[dict] | str,
|
|
87
|
+
goal_id: str = "",
|
|
88
|
+
shared_state: dict | str | None = None,
|
|
89
|
+
owner: str = "",
|
|
90
|
+
idempotency_key: str = "",
|
|
91
|
+
) -> dict | list | str:
|
|
92
|
+
return self.call(
|
|
93
|
+
"nexo_run_workflow",
|
|
94
|
+
{
|
|
95
|
+
"sid": sid,
|
|
96
|
+
"goal": goal,
|
|
97
|
+
"steps": steps if isinstance(steps, str) else json.dumps(steps, ensure_ascii=False),
|
|
98
|
+
"goal_id": goal_id,
|
|
99
|
+
"shared_state": shared_state if isinstance(shared_state, str) else json.dumps(shared_state or {}, ensure_ascii=False),
|
|
100
|
+
"owner": owner,
|
|
101
|
+
"idempotency_key": idempotency_key,
|
|
102
|
+
},
|
|
103
|
+
)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""OpenTelemetry observability for NEXO Brain — Fase 5 item 2.
|
|
2
|
+
|
|
3
|
+
Closes Fase 5 item 2 of NEXO-AUDIT-2026-04-11. The audit asked for
|
|
4
|
+
observability with OTEL / Langfuse / Phoenix integration. This module
|
|
5
|
+
is the soft-import host: it adds tracing primitives that NEXO can call
|
|
6
|
+
unconditionally, but only emit real spans when:
|
|
7
|
+
|
|
8
|
+
1. The opentelemetry-api package is installed in the user's environment
|
|
9
|
+
(`pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp`).
|
|
10
|
+
2. The OTEL_EXPORTER_OTLP_ENDPOINT environment variable is set
|
|
11
|
+
(or OTEL_SERVICE_NAME is set, indicating the user already
|
|
12
|
+
bootstrapped a tracer provider externally).
|
|
13
|
+
|
|
14
|
+
Otherwise every primitive is a no-op so the runtime cost on installs
|
|
15
|
+
without OTEL is exactly zero (no try/except per call site, no extra
|
|
16
|
+
import time on the hot path).
|
|
17
|
+
|
|
18
|
+
Why this design:
|
|
19
|
+
- NEXO core does NOT require opentelemetry as a hard dependency.
|
|
20
|
+
The 10k+ active users would have an extra 30 MB in their venv with
|
|
21
|
+
no benefit unless they opted in.
|
|
22
|
+
- Users who DO want telemetry get a single env var to flip on.
|
|
23
|
+
- The shape (span name, attributes, status) follows the OpenTelemetry
|
|
24
|
+
semantic conventions for "ai.tool" so dashboards in Langfuse,
|
|
25
|
+
Arize Phoenix, Honeycomb, Jaeger, and Grafana Tempo all render
|
|
26
|
+
NEXO traces with their built-in views.
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
|
|
30
|
+
from observability import tool_span
|
|
31
|
+
|
|
32
|
+
with tool_span("nexo_heartbeat", attributes={"sid": sid}) as span:
|
|
33
|
+
result = handle_heartbeat(sid, task)
|
|
34
|
+
if span is not None:
|
|
35
|
+
span.set_attribute("nexo.heartbeat.task", task[:200])
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
The `with` block always works whether or not OTEL is installed; the
|
|
39
|
+
span is None when telemetry is disabled.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import os
|
|
45
|
+
from contextlib import contextmanager
|
|
46
|
+
from typing import Any, Iterator
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── OTEL availability detection ──────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_otel_available: bool | None = None
|
|
53
|
+
_tracer = None # cached after first use
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _truthy_env(var: str) -> bool:
|
|
57
|
+
return bool((os.environ.get(var) or "").strip())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_otel_enabled() -> bool:
|
|
61
|
+
"""Return True if OpenTelemetry is installed AND a configuration is set.
|
|
62
|
+
|
|
63
|
+
The two activation conditions are:
|
|
64
|
+
- opentelemetry-api importable
|
|
65
|
+
- One of OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_SERVICE_NAME is set
|
|
66
|
+
(the latter signals an externally-bootstrapped tracer provider).
|
|
67
|
+
|
|
68
|
+
Cached after first call so the hot path is a single bool lookup.
|
|
69
|
+
"""
|
|
70
|
+
global _otel_available
|
|
71
|
+
if _otel_available is not None:
|
|
72
|
+
return _otel_available
|
|
73
|
+
|
|
74
|
+
if not (_truthy_env("OTEL_EXPORTER_OTLP_ENDPOINT") or _truthy_env("OTEL_SERVICE_NAME")):
|
|
75
|
+
_otel_available = False
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
import opentelemetry # noqa: F401
|
|
80
|
+
from opentelemetry import trace # noqa: F401
|
|
81
|
+
except ImportError:
|
|
82
|
+
_otel_available = False
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
_otel_available = True
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_tracer():
|
|
90
|
+
"""Return a cached opentelemetry.trace.Tracer or None when OTEL is off.
|
|
91
|
+
|
|
92
|
+
Lazy: only constructs the tracer the first time it is needed so
|
|
93
|
+
installs without OTEL never pay any import cost.
|
|
94
|
+
"""
|
|
95
|
+
global _tracer
|
|
96
|
+
if _tracer is not None:
|
|
97
|
+
return _tracer
|
|
98
|
+
if not is_otel_enabled():
|
|
99
|
+
return None
|
|
100
|
+
try:
|
|
101
|
+
from opentelemetry import trace
|
|
102
|
+
_tracer = trace.get_tracer("nexo-brain")
|
|
103
|
+
return _tracer
|
|
104
|
+
except Exception:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ── span context manager ─────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@contextmanager
|
|
112
|
+
def tool_span(
|
|
113
|
+
name: str,
|
|
114
|
+
*,
|
|
115
|
+
attributes: dict[str, Any] | None = None,
|
|
116
|
+
) -> Iterator[Any]:
|
|
117
|
+
"""Context manager that emits an OTEL span when telemetry is enabled.
|
|
118
|
+
|
|
119
|
+
The span name follows the OTEL semantic convention prefix "ai.tool."
|
|
120
|
+
so dashboards that already group by ai.tool.* automatically pick
|
|
121
|
+
NEXO traces up.
|
|
122
|
+
|
|
123
|
+
On success: status = OK.
|
|
124
|
+
On exception: status = ERROR with the exception message recorded as
|
|
125
|
+
an attribute, and the exception is re-raised so callers see it.
|
|
126
|
+
|
|
127
|
+
When telemetry is disabled, the context manager yields None and
|
|
128
|
+
does nothing else — the cost is one is_otel_enabled() bool lookup
|
|
129
|
+
plus the empty `with` block, which the Python compiler optimizes.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
name: short tool name. The full span name becomes "ai.tool.<name>".
|
|
133
|
+
attributes: optional dict of OTEL attributes to set on the span.
|
|
134
|
+
"""
|
|
135
|
+
if not is_otel_enabled():
|
|
136
|
+
yield None
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
tracer = get_tracer()
|
|
140
|
+
if tracer is None:
|
|
141
|
+
yield None
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
from opentelemetry import trace as _trace
|
|
146
|
+
span_name = f"ai.tool.{name}" if not name.startswith("ai.tool.") else name
|
|
147
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
148
|
+
try:
|
|
149
|
+
if attributes:
|
|
150
|
+
for key, value in attributes.items():
|
|
151
|
+
try:
|
|
152
|
+
span.set_attribute(key, value)
|
|
153
|
+
except Exception:
|
|
154
|
+
# Some values (e.g. dicts) are not OTEL-compatible.
|
|
155
|
+
try:
|
|
156
|
+
span.set_attribute(key, str(value)[:1000])
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
yield span
|
|
160
|
+
span.set_status(_trace.Status(_trace.StatusCode.OK))
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
try:
|
|
163
|
+
span.record_exception(exc)
|
|
164
|
+
span.set_status(
|
|
165
|
+
_trace.Status(_trace.StatusCode.ERROR, str(exc)[:300])
|
|
166
|
+
)
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
raise
|
|
170
|
+
except Exception:
|
|
171
|
+
# If anything goes wrong with the OTEL machinery itself, fall
|
|
172
|
+
# back to a no-op so the caller never sees a telemetry-induced
|
|
173
|
+
# exception. The original work still runs.
|
|
174
|
+
yield None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def record_tool_attributes(span: Any, attributes: dict[str, Any]) -> None:
|
|
178
|
+
"""Set OTEL attributes on a span if it is non-None and OTEL is enabled.
|
|
179
|
+
|
|
180
|
+
Convenience helper so callers do not need to write the `if span is
|
|
181
|
+
not None` guard at every call site.
|
|
182
|
+
"""
|
|
183
|
+
if span is None:
|
|
184
|
+
return
|
|
185
|
+
for key, value in (attributes or {}).items():
|
|
186
|
+
try:
|
|
187
|
+
span.set_attribute(key, value)
|
|
188
|
+
except Exception:
|
|
189
|
+
try:
|
|
190
|
+
span.set_attribute(key, str(value)[:1000])
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _reset_for_tests() -> None:
|
|
196
|
+
"""Test-only helper to reset the cached availability + tracer."""
|
|
197
|
+
global _otel_available, _tracer
|
|
198
|
+
_otel_available = None
|
|
199
|
+
_tracer = None
|