nexo-brain 7.1.1 → 7.1.3
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 +3 -2
- package/bin/nexo-brain.js +198 -92
- package/package.json +1 -1
- package/src/agent_runner.py +10 -8
- package/src/auto_close_sessions.py +19 -2
- package/src/auto_update.py +242 -28
- package/src/autonomy_mandate.py +260 -0
- package/src/bootstrap_docs.py +22 -1
- package/src/cli.py +211 -1
- package/src/cli_email.py +104 -73
- package/src/client_sync.py +22 -1
- package/src/cognitive/_core.py +5 -3
- package/src/core_prompts.py +50 -0
- package/src/cron_recovery.py +81 -7
- package/src/crons/manifest.json +57 -0
- package/src/crons/sync.py +95 -26
- package/src/dashboard/app.py +59 -0
- package/src/dashboard/templates/base.html +2 -0
- package/src/dashboard/templates/feature-disabled.html +27 -0
- package/src/db/_email_accounts.py +67 -18
- package/src/db/_fts.py +5 -5
- package/src/db/_personal_scripts.py +1 -1
- package/src/db/_skills.py +3 -3
- package/src/doctor/providers/runtime.py +35 -20
- package/src/email_config.py +18 -9
- package/src/enforcement_classifier.py +3 -12
- package/src/enforcement_engine.py +6 -9
- package/src/evolution_cycle.py +37 -149
- package/src/guardian_telemetry.py +3 -2
- package/src/hook_guardrails.py +61 -0
- package/src/hooks/capture-tool-logs.sh +11 -3
- package/src/hooks/daily-briefing-check.sh +7 -2
- package/src/hooks/heartbeat-enforcement.py +14 -1
- package/src/hooks/heartbeat-posttool.sh +2 -0
- package/src/hooks/heartbeat-user-msg.sh +2 -0
- package/src/hooks/inbox-hook.sh +6 -2
- package/src/hooks/post-compact.sh +12 -4
- package/src/hooks/pre-compact.sh +12 -4
- package/src/migrate_embeddings.py +5 -3
- package/src/nexo_migrate.py +3 -1
- package/src/plugin_loader.py +14 -5
- package/src/plugins/adaptive_mode.py +4 -1
- package/src/plugins/backup.py +32 -20
- package/src/plugins/evolution.py +2 -0
- package/src/plugins/memory_export.py +6 -1
- package/src/plugins/personal_plugins.py +17 -7
- package/src/plugins/personal_scripts.py +64 -3
- package/src/plugins/update.py +34 -14
- package/src/presets/entities_universal.json +67 -4
- package/src/product_mode.py +201 -0
- package/src/r14_correction_learning.py +5 -20
- package/src/r15_project_context.py +4 -10
- package/src/r16_declared_done.py +3 -16
- package/src/r17_promise_debt.py +3 -16
- package/src/r18_followup_autocomplete.py +5 -7
- package/src/r19_project_grep.py +5 -8
- package/src/r20_constant_change.py +5 -15
- package/src/r21_legacy_path.py +5 -7
- package/src/r22_personal_script.py +4 -8
- package/src/r23_ssh_without_atlas.py +4 -11
- package/src/r23b_deploy_vhost.py +7 -6
- package/src/r23c_cwd_mismatch.py +7 -6
- package/src/r23d_chown_chmod_recursive.py +6 -6
- package/src/r23e_force_push_main.py +5 -6
- package/src/r23f_db_no_where.py +5 -6
- package/src/r23g_secrets_in_output.py +5 -5
- package/src/r23h_shebang_mismatch.py +6 -5
- package/src/r23i_auto_deploy_ignored.py +5 -6
- package/src/r23j_global_install.py +5 -6
- package/src/r23k_script_duplicates_skill.py +7 -6
- package/src/r23l_resource_collision.py +7 -6
- package/src/r23m_message_duplicate.py +6 -5
- package/src/r24_stale_memory.py +4 -9
- package/src/r25_nora_maria_read_only.py +5 -10
- package/src/r34_identity_coherence.py +6 -13
- package/src/r_catalog.py +3 -7
- package/src/resonance_map.py +13 -13
- package/src/runtime_power.py +29 -80
- package/src/script_registry.py +236 -6
- package/src/scripts/check-context.py +8 -25
- package/src/scripts/deep-sleep/extract.py +9 -16
- package/src/scripts/nexo-auto-update.py +27 -4
- package/src/scripts/nexo-catchup.py +9 -19
- package/src/scripts/nexo-cognitive-decay.py +26 -3
- package/src/scripts/nexo-daily-self-audit.py +50 -51
- package/src/scripts/nexo-email-migrate-config.py +30 -11
- package/src/scripts/nexo-email-monitor.py +97 -238
- package/src/scripts/nexo-followup-runner.py +70 -133
- package/src/scripts/nexo-hook-record.py +1 -1
- package/src/scripts/nexo-immune.py +6 -31
- package/src/scripts/nexo-impact-scorer.py +27 -4
- package/src/scripts/nexo-learning-housekeep.py +26 -3
- package/src/scripts/nexo-learning-validator.py +34 -32
- package/src/scripts/nexo-migrate.py +28 -12
- package/src/scripts/nexo-morning-agent.py +9 -23
- package/src/scripts/nexo-outcome-checker.py +27 -4
- package/src/scripts/nexo-postmortem-consolidator.py +30 -62
- package/src/scripts/nexo-pre-commit.py +28 -0
- package/src/scripts/nexo-proactive-dashboard.py +27 -0
- package/src/scripts/nexo-reflection.py +33 -3
- package/src/scripts/nexo-runtime-preflight.py +27 -2
- package/src/scripts/nexo-send-reply.py +10 -8
- package/src/scripts/nexo-sleep.py +11 -25
- package/src/scripts/nexo-synthesis.py +7 -40
- package/src/scripts/nexo-watchdog-smoke.py +30 -1
- package/src/scripts/nexo-watchdog.sh +23 -17
- package/src/scripts/phase_guardian_analysis.py +27 -4
- package/src/server.py +14 -3
- package/src/storage_router.py +8 -6
- package/src/t4_llm_gate.py +17 -84
- package/src/tools_drive.py +5 -13
- package/src/tools_guardian.py +3 -4
- package/src/tools_menu.py +2 -2
- package/src/tools_reminders_crud.py +17 -0
- package/src/tools_sessions.py +1 -4
- package/src/user_context.py +3 -6
- package/src/user_data_portability.py +141 -44
- package/templates/CLAUDE.md.template +11 -3
- package/templates/CODEX.AGENTS.md.template +11 -3
- package/templates/core-prompts/catchup-assessment.md +19 -0
- package/templates/core-prompts/check-context.md +24 -0
- package/templates/core-prompts/daily-self-audit.md +42 -0
- package/templates/core-prompts/daily-synthesis.md +40 -0
- package/templates/core-prompts/deep-sleep-extract-json-conversion.md +6 -0
- package/templates/core-prompts/deep-sleep-extract-json-output.md +8 -0
- package/templates/core-prompts/drive-signal-classifier-system.md +4 -0
- package/templates/core-prompts/drive-signal-classifier-user.md +6 -0
- package/templates/core-prompts/email-monitor.md +202 -0
- package/templates/core-prompts/enforcement-classifier-retry.md +1 -0
- package/templates/core-prompts/enforcement-classifier-strict.md +1 -0
- package/templates/core-prompts/evolution-public-contribution.md +32 -0
- package/templates/core-prompts/evolution-public-pr-review.md +38 -0
- package/templates/core-prompts/evolution-weekly.md +71 -0
- package/templates/core-prompts/followup-runner-operator-attention-context.md +4 -0
- package/templates/core-prompts/followup-runner-operator-attention-question.md +1 -0
- package/templates/core-prompts/followup-runner.md +74 -0
- package/templates/core-prompts/immune-triage.md +31 -0
- package/templates/core-prompts/interactive-startup.md +1 -0
- package/templates/core-prompts/json-object-only.md +1 -0
- package/templates/core-prompts/learning-validator.md +25 -0
- package/templates/core-prompts/morning-agent-json-output.md +1 -0
- package/templates/core-prompts/morning-agent.md +23 -0
- package/templates/core-prompts/postmortem-consolidator.md +60 -0
- package/templates/core-prompts/r-catalog.md +1 -0
- package/templates/core-prompts/r13-pre-edit-guard-injection.md +1 -0
- package/templates/core-prompts/r14-correction-learning-injection.md +1 -0
- package/templates/core-prompts/r14-correction-learning-question.md +1 -0
- package/templates/core-prompts/r15-project-context-injection.md +1 -0
- package/templates/core-prompts/r16-declared-done-injection.md +1 -0
- package/templates/core-prompts/r16-declared-done-question.md +1 -0
- package/templates/core-prompts/r17-promise-debt-injection.md +1 -0
- package/templates/core-prompts/r17-promise-debt-question.md +1 -0
- package/templates/core-prompts/r18-followup-autocomplete-injection.md +3 -0
- package/templates/core-prompts/r19-project-grep-injection.md +1 -0
- package/templates/core-prompts/r20-constant-change-injection.md +1 -0
- package/templates/core-prompts/r20-constant-change-question.md +1 -0
- package/templates/core-prompts/r21-legacy-path-injection.md +1 -0
- package/templates/core-prompts/r22-personal-script-injection.md +1 -0
- package/templates/core-prompts/r23-ssh-without-atlas-injection.md +1 -0
- package/templates/core-prompts/r23b-deploy-vhost-injection.md +1 -0
- package/templates/core-prompts/r23c-cwd-mismatch-injection.md +1 -0
- package/templates/core-prompts/r23d-chown-chmod-recursive-injection.md +1 -0
- package/templates/core-prompts/r23e-force-push-main-injection.md +1 -0
- package/templates/core-prompts/r23f-db-no-where-injection.md +1 -0
- package/templates/core-prompts/r23g-secrets-in-output-injection.md +1 -0
- package/templates/core-prompts/r23h-shebang-mismatch-injection.md +1 -0
- package/templates/core-prompts/r23i-auto-deploy-ignored-injection.md +1 -0
- package/templates/core-prompts/r23j-global-install-injection.md +1 -0
- package/templates/core-prompts/r23k-script-duplicates-skill-injection.md +1 -0
- package/templates/core-prompts/r23l-resource-collision-injection.md +1 -0
- package/templates/core-prompts/r23m-message-duplicate-injection.md +1 -0
- package/templates/core-prompts/r24-stale-memory-injection.md +1 -0
- package/templates/core-prompts/r25-read-only-host-injection.md +1 -0
- package/templates/core-prompts/r34-identity-coherence-probe.md +1 -0
- package/templates/core-prompts/r34-identity-coherence-question.md +1 -0
- package/templates/core-prompts/sleep.md +25 -0
- package/templates/core-prompts/t4-r15-project-context-gate.md +14 -0
- package/templates/core-prompts/t4-r23e-force-push-gate.md +14 -0
- package/templates/core-prompts/t4-r23f-db-no-where-gate.md +14 -0
- package/templates/core-prompts/t4-r23h-shebang-mismatch-gate.md +14 -0
- package/templates/email-template.md +55 -0
- package/templates/nexo_helper.py +31 -13
- package/templates/plugin-template.py +3 -3
- package/templates/skill-template.md +2 -1
package/src/agent_runner.py
CHANGED
|
@@ -10,6 +10,7 @@ import shutil
|
|
|
10
10
|
import subprocess
|
|
11
11
|
import tempfile
|
|
12
12
|
import time
|
|
13
|
+
from functools import lru_cache
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
try:
|
|
@@ -29,6 +30,7 @@ from client_preferences import (
|
|
|
29
30
|
resolve_client_runtime_profile,
|
|
30
31
|
resolve_terminal_client,
|
|
31
32
|
)
|
|
33
|
+
from core_prompts import render_core_prompt
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
@@ -39,13 +41,6 @@ MODEL_PRICING_USD_PER_1M = {
|
|
|
39
41
|
"gpt-5.4": {"input": 1.25, "cached_input": 0.125, "output": 10.0},
|
|
40
42
|
"gpt-5.4-mini": {"input": 0.25, "cached_input": 0.025, "output": 2.0},
|
|
41
43
|
}
|
|
42
|
-
INTERACTIVE_STARTUP_PROMPT = (
|
|
43
|
-
"Start as NEXO for this session now. Use the managed bootstrap already installed "
|
|
44
|
-
"for this client, run nexo_startup and nexo_heartbeat for this first turn, then "
|
|
45
|
-
"reply with one concise startup status in the user's language."
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
44
|
class AgentRunnerError(RuntimeError):
|
|
50
45
|
"""Base exception for runner failures."""
|
|
51
46
|
|
|
@@ -438,10 +433,15 @@ def _codex_interactive_launch_flags() -> list[str]:
|
|
|
438
433
|
return ["--sandbox", "danger-full-access", "--ask-for-approval", "never"]
|
|
439
434
|
|
|
440
435
|
|
|
436
|
+
@lru_cache(maxsize=1)
|
|
437
|
+
def _interactive_startup_prompt_text() -> str:
|
|
438
|
+
return render_core_prompt("interactive-startup")
|
|
439
|
+
|
|
440
|
+
|
|
441
441
|
def _interactive_startup_prompt(client: str) -> str:
|
|
442
442
|
client_key = normalize_client_key(client)
|
|
443
443
|
if client_key in {CLIENT_CLAUDE_CODE, CLIENT_CODEX}:
|
|
444
|
-
return
|
|
444
|
+
return _interactive_startup_prompt_text()
|
|
445
445
|
return ""
|
|
446
446
|
|
|
447
447
|
|
|
@@ -1206,6 +1206,7 @@ def probe_automation_backend(
|
|
|
1206
1206
|
backend: str | None = None,
|
|
1207
1207
|
cwd: str | os.PathLike[str] | None = None,
|
|
1208
1208
|
timeout: int = 60,
|
|
1209
|
+
caller: str = "automation_probe",
|
|
1209
1210
|
) -> dict:
|
|
1210
1211
|
selected_backend = backend or resolve_automation_backend()
|
|
1211
1212
|
if selected_backend == BACKEND_NONE:
|
|
@@ -1221,6 +1222,7 @@ def probe_automation_backend(
|
|
|
1221
1222
|
cwd=cwd,
|
|
1222
1223
|
timeout=timeout,
|
|
1223
1224
|
output_format="text",
|
|
1225
|
+
caller=caller,
|
|
1224
1226
|
)
|
|
1225
1227
|
except AutomationBackendUnavailableError as exc:
|
|
1226
1228
|
return {
|
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import os
|
|
11
11
|
import sys
|
|
12
12
|
import datetime
|
|
13
|
+
from pathlib import Path
|
|
13
14
|
|
|
14
15
|
# Ensure imports work both from ``src/auto_close_sessions.py`` and from the
|
|
15
16
|
# packaged runtime copy under ``core/scripts/auto_close_sessions.py``.
|
|
@@ -27,10 +28,26 @@ from db import (
|
|
|
27
28
|
get_orphan_sessions, read_checkpoint, write_session_diary, now_epoch,
|
|
28
29
|
SESSION_STALE_SECONDS,
|
|
29
30
|
)
|
|
31
|
+
try:
|
|
32
|
+
import paths
|
|
33
|
+
except ModuleNotFoundError as exc:
|
|
34
|
+
if getattr(exc, "name", "") != "paths":
|
|
35
|
+
raise
|
|
36
|
+
|
|
37
|
+
class _PathsFallback:
|
|
38
|
+
@staticmethod
|
|
39
|
+
def operations_dir():
|
|
40
|
+
return Path(os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))) / "operations"
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def coordination_dir():
|
|
44
|
+
return Path(os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))) / "coordination"
|
|
45
|
+
|
|
46
|
+
paths = _PathsFallback()
|
|
30
47
|
|
|
31
48
|
NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
|
|
32
|
-
LOG_DIR =
|
|
33
|
-
AUTO_CLOSE_LOG =
|
|
49
|
+
LOG_DIR = str(paths.operations_dir() / "tool-logs")
|
|
50
|
+
AUTO_CLOSE_LOG = str(paths.coordination_dir() / "auto-close.log")
|
|
34
51
|
|
|
35
52
|
|
|
36
53
|
def get_tool_log_summary(sid: str) -> str:
|
package/src/auto_update.py
CHANGED
|
@@ -20,6 +20,17 @@ import threading
|
|
|
20
20
|
import time
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
|
+
try:
|
|
24
|
+
from product_mode import enforce_desktop_product_contract
|
|
25
|
+
except ModuleNotFoundError as exc:
|
|
26
|
+
if getattr(exc, "name", "") != "product_mode":
|
|
27
|
+
raise
|
|
28
|
+
_core_runtime = Path(__file__).resolve().parent / "core"
|
|
29
|
+
if _core_runtime.is_dir():
|
|
30
|
+
core_path = str(_core_runtime)
|
|
31
|
+
if core_path not in sys.path:
|
|
32
|
+
sys.path.insert(0, core_path)
|
|
33
|
+
from product_mode import enforce_desktop_product_contract
|
|
23
34
|
from runtime_home import export_resolved_nexo_home, managed_nexo_home
|
|
24
35
|
|
|
25
36
|
try:
|
|
@@ -87,6 +98,13 @@ _CORE_SCRIPT_RUNTIME_FILES = frozenset({
|
|
|
87
98
|
".watchdog-fails",
|
|
88
99
|
".watchdog-nexo-repair.lock",
|
|
89
100
|
})
|
|
101
|
+
_LEGACY_PERSONAL_BRAIN_DB_STUBS = frozenset({
|
|
102
|
+
"brain.db",
|
|
103
|
+
"cron_runs.db",
|
|
104
|
+
"nexo.db",
|
|
105
|
+
"nexo_brain.db",
|
|
106
|
+
"runtime.db",
|
|
107
|
+
})
|
|
90
108
|
|
|
91
109
|
|
|
92
110
|
def _log(msg: str):
|
|
@@ -188,6 +206,101 @@ def _cleanup_legacy_root_db_stubs(runtime_root: Path = NEXO_HOME, *, dry_run: bo
|
|
|
188
206
|
return report
|
|
189
207
|
|
|
190
208
|
|
|
209
|
+
def _sqlite_table_names(db_path: Path) -> list[str] | None:
|
|
210
|
+
"""Return user tables for a SQLite file or ``None`` when unreadable."""
|
|
211
|
+
import sqlite3
|
|
212
|
+
|
|
213
|
+
conn = None
|
|
214
|
+
try:
|
|
215
|
+
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
|
216
|
+
rows = conn.execute(
|
|
217
|
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
|
218
|
+
).fetchall()
|
|
219
|
+
return [str(row[0]) for row in rows if row and row[0]]
|
|
220
|
+
except Exception:
|
|
221
|
+
return None
|
|
222
|
+
finally:
|
|
223
|
+
if conn is not None:
|
|
224
|
+
try:
|
|
225
|
+
conn.close()
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _cleanup_empty_personal_brain_db_stubs(runtime_root: Path = NEXO_HOME, *, dry_run: bool = False) -> dict:
|
|
231
|
+
"""Archive obsolete DB shells from ``personal/brain`` after F0.6 migration.
|
|
232
|
+
|
|
233
|
+
Older layouts and partial migrations sometimes left duplicate DB files in
|
|
234
|
+
``personal/brain``. Post-F0.6 those DBs are not canonical anymore: the live
|
|
235
|
+
runtime DBs live under ``runtime/data``. This helper only archives files
|
|
236
|
+
from a known allowlist when they are clearly empty residue:
|
|
237
|
+
- zero-byte files, or
|
|
238
|
+
- SQLite shells with zero tables
|
|
239
|
+
|
|
240
|
+
Any file with real tables is left untouched.
|
|
241
|
+
"""
|
|
242
|
+
runtime_root = Path(runtime_root)
|
|
243
|
+
personal_brain_dir = runtime_root / "personal" / "brain"
|
|
244
|
+
report = {
|
|
245
|
+
"ok": True,
|
|
246
|
+
"dry_run": dry_run,
|
|
247
|
+
"candidates": [],
|
|
248
|
+
"archived": [],
|
|
249
|
+
"skipped": [],
|
|
250
|
+
"errors": [],
|
|
251
|
+
}
|
|
252
|
+
if not personal_brain_dir.is_dir():
|
|
253
|
+
return report
|
|
254
|
+
|
|
255
|
+
backup_root: Path | None = None
|
|
256
|
+
for name in sorted(_LEGACY_PERSONAL_BRAIN_DB_STUBS):
|
|
257
|
+
candidate = personal_brain_dir / name
|
|
258
|
+
if not candidate.is_file():
|
|
259
|
+
continue
|
|
260
|
+
try:
|
|
261
|
+
size = int(candidate.stat().st_size)
|
|
262
|
+
except OSError as exc:
|
|
263
|
+
report["errors"].append({"path": str(candidate), "error": str(exc)})
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
reason = ""
|
|
267
|
+
if size == 0:
|
|
268
|
+
reason = "zero-byte"
|
|
269
|
+
else:
|
|
270
|
+
tables = _sqlite_table_names(candidate)
|
|
271
|
+
if tables is None:
|
|
272
|
+
report["skipped"].append({"path": str(candidate), "reason": "uninspectable"})
|
|
273
|
+
continue
|
|
274
|
+
if tables:
|
|
275
|
+
report["skipped"].append({
|
|
276
|
+
"path": str(candidate),
|
|
277
|
+
"reason": "has-tables",
|
|
278
|
+
"table_count": len(tables),
|
|
279
|
+
})
|
|
280
|
+
continue
|
|
281
|
+
reason = "sqlite-empty-shell"
|
|
282
|
+
|
|
283
|
+
report["candidates"].append({"path": str(candidate), "size": size, "reason": reason})
|
|
284
|
+
if dry_run:
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
if backup_root is None:
|
|
288
|
+
timestamp = time.strftime("%Y-%m-%d-%H%M%S", time.gmtime())
|
|
289
|
+
backup_root = paths.backups_dir() / f"legacy-personal-brain-db-stubs-{timestamp}"
|
|
290
|
+
backup_root.mkdir(parents=True, exist_ok=True)
|
|
291
|
+
target = backup_root / candidate.name
|
|
292
|
+
try:
|
|
293
|
+
shutil.move(str(candidate), str(target))
|
|
294
|
+
report["archived"].append({
|
|
295
|
+
"path": str(candidate),
|
|
296
|
+
"backup_path": str(target),
|
|
297
|
+
"reason": reason,
|
|
298
|
+
})
|
|
299
|
+
except Exception as exc:
|
|
300
|
+
report["errors"].append({"path": str(candidate), "error": str(exc)})
|
|
301
|
+
return report
|
|
302
|
+
|
|
303
|
+
|
|
191
304
|
def _validate_db_backup(source_db: Path, backup_db: Path) -> dict:
|
|
192
305
|
"""Check that a backup preserves non-empty critical tables from the source DB."""
|
|
193
306
|
report = {
|
|
@@ -378,6 +491,7 @@ def _runtime_cli_wrapper_text() -> str:
|
|
|
378
491
|
'fi\n'
|
|
379
492
|
'NEXO_HOME="$RUNTIME_HOME"\n'
|
|
380
493
|
'export NEXO_HOME\n'
|
|
494
|
+
'export PYTHONDONTWRITEBYTECODE=1\n'
|
|
381
495
|
'resolve_code_dir() {\n'
|
|
382
496
|
' if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/cli.py" ]; then\n'
|
|
383
497
|
' printf \'%s\\n\' "${NEXO_CODE%/}"\n'
|
|
@@ -807,6 +921,17 @@ def _download_local_classifier_model() -> tuple[bool, str]:
|
|
|
807
921
|
return True, ""
|
|
808
922
|
|
|
809
923
|
|
|
924
|
+
def _local_classifier_install_command() -> list[str]:
|
|
925
|
+
in_virtualenv = bool(os.environ.get("VIRTUAL_ENV")) or (
|
|
926
|
+
getattr(sys, "prefix", "") != getattr(sys, "base_prefix", getattr(sys, "prefix", ""))
|
|
927
|
+
)
|
|
928
|
+
cmd = [sys.executable, "-m", "pip", "install"]
|
|
929
|
+
if not in_virtualenv:
|
|
930
|
+
cmd.append("--user")
|
|
931
|
+
cmd.extend(CLASSIFIER_INSTALL_PACKAGES)
|
|
932
|
+
return cmd
|
|
933
|
+
|
|
934
|
+
|
|
810
935
|
def _install_local_classifier_worker() -> None:
|
|
811
936
|
from classifier_local import MODEL_REVISION
|
|
812
937
|
|
|
@@ -832,7 +957,7 @@ def _install_local_classifier_worker() -> None:
|
|
|
832
957
|
})
|
|
833
958
|
return
|
|
834
959
|
|
|
835
|
-
install_cmd =
|
|
960
|
+
install_cmd = _local_classifier_install_command()
|
|
836
961
|
_write_classifier_install_log("[classifier-install] " + " ".join(install_cmd))
|
|
837
962
|
try:
|
|
838
963
|
result = subprocess.run(
|
|
@@ -1441,21 +1566,24 @@ def _migrate_effort_to_resonance(dest: Path = NEXO_HOME) -> list[str]:
|
|
|
1441
1566
|
|
|
1442
1567
|
def _relocate_resonance_tiers_contract(dest: Path = NEXO_HOME) -> list[str]:
|
|
1443
1568
|
"""Ensure ``resonance_tiers.json`` lives at the public contract path
|
|
1444
|
-
``NEXO_HOME/brain/resonance_tiers.json`` and purge the legacy
|
|
1445
|
-
``NEXO_HOME/resonance_tiers.json``.
|
|
1569
|
+
``NEXO_HOME/personal/brain/resonance_tiers.json`` and purge the legacy
|
|
1570
|
+
copy at ``NEXO_HOME/resonance_tiers.json``.
|
|
1446
1571
|
|
|
1447
1572
|
Context: v6.0.0 defined the public contract (read by NEXO Desktop) as
|
|
1448
|
-
``~/.nexo/brain/resonance_tiers.json`` but the
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1573
|
+
``~/.nexo/brain/resonance_tiers.json`` but the F0.6 canonical location is
|
|
1574
|
+
``~/.nexo/personal/brain/resonance_tiers.json``. Older installers also
|
|
1575
|
+
copied the file to ``~/.nexo/resonance_tiers.json`` (legacy flat-file
|
|
1576
|
+
layout), so Desktop failed with *"NEXO Brain contract missing"* until the
|
|
1577
|
+
user moved the file by hand. The runtime now publishes to the canonical
|
|
1578
|
+
F0.6 brain dir while legacy ``~/.nexo/brain`` remains only a compatibility
|
|
1579
|
+
alias when present.
|
|
1580
|
+
|
|
1581
|
+
Idempotent: no-op once the contract file is in ``personal/brain`` and the
|
|
1582
|
+
legacy flat-file copy is gone. Never raises — migration must not block an
|
|
1583
|
+
update.
|
|
1456
1584
|
"""
|
|
1457
1585
|
actions: list[str] = []
|
|
1458
|
-
brain_dir = dest / "brain"
|
|
1586
|
+
brain_dir = dest / "personal" / "brain"
|
|
1459
1587
|
contract_path = brain_dir / "resonance_tiers.json"
|
|
1460
1588
|
legacy_path = dest / "resonance_tiers.json"
|
|
1461
1589
|
|
|
@@ -1465,7 +1593,8 @@ def _relocate_resonance_tiers_contract(dest: Path = NEXO_HOME) -> list[str]:
|
|
|
1465
1593
|
actions.append(f"resonance-contract-relocate-warning:mkdir:{exc.__class__.__name__}")
|
|
1466
1594
|
return actions
|
|
1467
1595
|
|
|
1468
|
-
# If the contract already exists in brain
|
|
1596
|
+
# If the contract already exists in the canonical brain dir, just drop the
|
|
1597
|
+
# legacy flat-file copy.
|
|
1469
1598
|
if contract_path.is_file():
|
|
1470
1599
|
if legacy_path.is_file():
|
|
1471
1600
|
try:
|
|
@@ -1475,7 +1604,8 @@ def _relocate_resonance_tiers_contract(dest: Path = NEXO_HOME) -> list[str]:
|
|
|
1475
1604
|
actions.append(f"resonance-contract-relocate-warning:unlink:{exc.__class__.__name__}")
|
|
1476
1605
|
return actions
|
|
1477
1606
|
|
|
1478
|
-
# Contract missing from brain
|
|
1607
|
+
# Contract missing from the canonical brain dir — promote the legacy file
|
|
1608
|
+
# if present.
|
|
1479
1609
|
if legacy_path.is_file():
|
|
1480
1610
|
try:
|
|
1481
1611
|
contract_path.write_bytes(legacy_path.read_bytes())
|
|
@@ -1960,7 +2090,6 @@ def _run_db_migrations() -> bool:
|
|
|
1960
2090
|
# personal_scripts.path + LaunchAgent plists, then write the
|
|
1961
2091
|
# F0.6 marker. Idempotent: returns immediately when already F0.6.
|
|
1962
2092
|
_maybe_migrate_to_f06_layout()
|
|
1963
|
-
_ensure_f06_legacy_shims()
|
|
1964
2093
|
try:
|
|
1965
2094
|
_rewrite_f06_launch_agents()
|
|
1966
2095
|
except Exception:
|
|
@@ -2128,6 +2257,49 @@ def _f06_packaged_code_file_targets() -> list[tuple[str, Path]]:
|
|
|
2128
2257
|
return targets
|
|
2129
2258
|
|
|
2130
2259
|
|
|
2260
|
+
def _f06_live_legacy_paths() -> list[Path]:
|
|
2261
|
+
"""Return real pre-F0.6 paths that still need migration.
|
|
2262
|
+
|
|
2263
|
+
Symlink shims do not count here. We only care about physical files or
|
|
2264
|
+
directories that keep the flat layout alive as a second source of truth.
|
|
2265
|
+
"""
|
|
2266
|
+
legacy_anchors = [
|
|
2267
|
+
NEXO_HOME / sub
|
|
2268
|
+
for sub in (
|
|
2269
|
+
"scripts",
|
|
2270
|
+
"brain",
|
|
2271
|
+
"data",
|
|
2272
|
+
"operations",
|
|
2273
|
+
"logs",
|
|
2274
|
+
"backups",
|
|
2275
|
+
"memory",
|
|
2276
|
+
"cognitive",
|
|
2277
|
+
"coordination",
|
|
2278
|
+
"exports",
|
|
2279
|
+
"nexo-email",
|
|
2280
|
+
"doctor",
|
|
2281
|
+
"snapshots",
|
|
2282
|
+
"crons",
|
|
2283
|
+
"skills",
|
|
2284
|
+
"plugins",
|
|
2285
|
+
"hooks",
|
|
2286
|
+
"rules",
|
|
2287
|
+
"db",
|
|
2288
|
+
"dashboard",
|
|
2289
|
+
"skills-core",
|
|
2290
|
+
)
|
|
2291
|
+
]
|
|
2292
|
+
present = [p for p in legacy_anchors if p.exists() and not p.is_symlink()]
|
|
2293
|
+
if present:
|
|
2294
|
+
return present
|
|
2295
|
+
live_files: list[Path] = []
|
|
2296
|
+
for legacy_name, _canonical in _f06_packaged_code_file_targets():
|
|
2297
|
+
candidate = NEXO_HOME / legacy_name
|
|
2298
|
+
if candidate.exists() and not candidate.is_symlink():
|
|
2299
|
+
live_files.append(candidate)
|
|
2300
|
+
return live_files
|
|
2301
|
+
|
|
2302
|
+
|
|
2131
2303
|
def _promote_packaged_runtime_code_to_core() -> None:
|
|
2132
2304
|
"""Move packaged runtime code out of ``~/.nexo`` root into ``core/``.
|
|
2133
2305
|
|
|
@@ -2424,6 +2596,23 @@ def _rewrite_f06_launch_agents() -> int:
|
|
|
2424
2596
|
return rewrites
|
|
2425
2597
|
|
|
2426
2598
|
|
|
2599
|
+
def _cleanup_f06_root_residue() -> None:
|
|
2600
|
+
"""Remove root-level cache artifacts that should not survive in F0.6 installs."""
|
|
2601
|
+
residue_targets = [
|
|
2602
|
+
NEXO_HOME / "__pycache__",
|
|
2603
|
+
]
|
|
2604
|
+
for target in residue_targets:
|
|
2605
|
+
try:
|
|
2606
|
+
if not target.exists() and not target.is_symlink():
|
|
2607
|
+
continue
|
|
2608
|
+
if target.is_symlink() or target.is_file():
|
|
2609
|
+
target.unlink(missing_ok=True)
|
|
2610
|
+
continue
|
|
2611
|
+
shutil.rmtree(target, ignore_errors=True)
|
|
2612
|
+
except Exception as exc:
|
|
2613
|
+
_log(f"[F0.6] residue cleanup skipped for {target}: {exc}")
|
|
2614
|
+
|
|
2615
|
+
|
|
2427
2616
|
def _maybe_migrate_to_f06_layout() -> None:
|
|
2428
2617
|
"""Plan F0.6 — one-shot physical layout migration. Idempotent.
|
|
2429
2618
|
|
|
@@ -2449,7 +2638,9 @@ def _maybe_migrate_to_f06_layout() -> None:
|
|
|
2449
2638
|
current = ""
|
|
2450
2639
|
if current == "F0.6":
|
|
2451
2640
|
_promote_packaged_runtime_code_to_core()
|
|
2452
|
-
|
|
2641
|
+
if _f06_live_legacy_paths():
|
|
2642
|
+
_ensure_f06_legacy_shims()
|
|
2643
|
+
_cleanup_f06_root_residue()
|
|
2453
2644
|
try:
|
|
2454
2645
|
_rewrite_f06_launch_agents()
|
|
2455
2646
|
except Exception:
|
|
@@ -2457,14 +2648,7 @@ def _maybe_migrate_to_f06_layout() -> None:
|
|
|
2457
2648
|
return # already migrated
|
|
2458
2649
|
# Refuse to migrate from a non-NEXO_HOME or a fresh install
|
|
2459
2650
|
# without legacy dirs to move.
|
|
2460
|
-
|
|
2461
|
-
("scripts", "brain", "data", "operations", "logs", "backups",
|
|
2462
|
-
"memory", "cognitive", "coordination", "exports",
|
|
2463
|
-
"nexo-email", "doctor", "snapshots", "crons",
|
|
2464
|
-
"skills", "plugins", "hooks", "rules", "db", "dashboard", "skills-core")]
|
|
2465
|
-
present = [p for p in legacy_anchors if p.exists() and not p.is_symlink()]
|
|
2466
|
-
if not present:
|
|
2467
|
-
present = [NEXO_HOME / name for name, _ in _f06_packaged_code_file_targets() if (NEXO_HOME / name).exists()]
|
|
2651
|
+
present = _f06_live_legacy_paths()
|
|
2468
2652
|
if not present:
|
|
2469
2653
|
# Fresh install — write marker and return.
|
|
2470
2654
|
try:
|
|
@@ -2671,6 +2855,7 @@ def _maybe_migrate_to_f06_layout() -> None:
|
|
|
2671
2855
|
except Exception as e:
|
|
2672
2856
|
_log(f"[F0.6] marker write failed: {e}")
|
|
2673
2857
|
_ensure_f06_legacy_shims()
|
|
2858
|
+
_cleanup_f06_root_residue()
|
|
2674
2859
|
try:
|
|
2675
2860
|
rewritten = _rewrite_f06_launch_agents()
|
|
2676
2861
|
if rewritten:
|
|
@@ -3407,7 +3592,14 @@ def _auto_update_check_locked() -> dict:
|
|
|
3407
3592
|
except Exception as e:
|
|
3408
3593
|
_log(f"evolution-objective.json backfill error: {e}")
|
|
3409
3594
|
|
|
3410
|
-
|
|
3595
|
+
try:
|
|
3596
|
+
desktop_contract = enforce_desktop_product_contract(source="auto_update")
|
|
3597
|
+
if desktop_contract.get("applied") and desktop_contract.get("changed_objective"):
|
|
3598
|
+
_log("Desktop product contract enforced: evolution disabled")
|
|
3599
|
+
except Exception as e:
|
|
3600
|
+
_log(f"desktop product contract error: {e}")
|
|
3601
|
+
|
|
3602
|
+
# Backfill installed core/scripts/ for existing installs
|
|
3411
3603
|
try:
|
|
3412
3604
|
scripts_dest = paths.core_scripts_dir()
|
|
3413
3605
|
# Deduce NEXO_CODE: env var first, then from __file__ (auto_update.py is in src/)
|
|
@@ -3422,7 +3614,7 @@ def _auto_update_check_locked() -> dict:
|
|
|
3422
3614
|
dest = scripts_dest / f.name
|
|
3423
3615
|
if f.is_file() and not dest.exists():
|
|
3424
3616
|
shutil.copy2(str(f), str(dest))
|
|
3425
|
-
_log("Backfilled NEXO_HOME/scripts
|
|
3617
|
+
_log("Backfilled NEXO_HOME/core/scripts from repo scripts for existing install")
|
|
3426
3618
|
except Exception as e:
|
|
3427
3619
|
_log(f"scripts backfill error: {e}")
|
|
3428
3620
|
|
|
@@ -4086,7 +4278,6 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
|
|
|
4086
4278
|
try:
|
|
4087
4279
|
_emit_progress(progress_fn, "Canonicalizing packaged runtime layout...")
|
|
4088
4280
|
_maybe_migrate_to_f06_layout()
|
|
4089
|
-
_ensure_f06_legacy_shims()
|
|
4090
4281
|
try:
|
|
4091
4282
|
_rewrite_f06_launch_agents()
|
|
4092
4283
|
except Exception:
|
|
@@ -4103,6 +4294,22 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
|
|
|
4103
4294
|
except Exception as exc:
|
|
4104
4295
|
actions.append(f"legacy-root-db-stubs-warning:{exc.__class__.__name__}")
|
|
4105
4296
|
|
|
4297
|
+
try:
|
|
4298
|
+
personal_brain_cleanup = _cleanup_empty_personal_brain_db_stubs(dest, dry_run=False)
|
|
4299
|
+
archived = len(personal_brain_cleanup.get("archived", []) or [])
|
|
4300
|
+
if archived:
|
|
4301
|
+
actions.append(f"legacy-personal-brain-db-stubs-archived:{archived}")
|
|
4302
|
+
except Exception as exc:
|
|
4303
|
+
actions.append(f"legacy-personal-brain-db-stubs-warning:{exc.__class__.__name__}")
|
|
4304
|
+
|
|
4305
|
+
try:
|
|
4306
|
+
_emit_progress(progress_fn, "Applying Desktop product contract...")
|
|
4307
|
+
contract = enforce_desktop_product_contract(source="runtime_post_sync")
|
|
4308
|
+
if contract.get("applied"):
|
|
4309
|
+
actions.append("desktop-product-contract")
|
|
4310
|
+
except Exception as exc:
|
|
4311
|
+
actions.append(f"desktop-product-contract-warning:{exc.__class__.__name__}")
|
|
4312
|
+
|
|
4106
4313
|
code_root = _runtime_code_dir(dest)
|
|
4107
4314
|
env = {**os.environ, "NEXO_HOME": str(dest), "NEXO_CODE": str(code_root)}
|
|
4108
4315
|
|
|
@@ -4344,6 +4551,13 @@ def _personal_schedule_reconcile_summary(reconcile_result: dict) -> tuple[list[s
|
|
|
4344
4551
|
actions: list[str] = []
|
|
4345
4552
|
parts: list[str] = []
|
|
4346
4553
|
|
|
4554
|
+
renamed = reconcile_result.get("renamed_legacy_filenames", {})
|
|
4555
|
+
if isinstance(renamed, dict):
|
|
4556
|
+
renamed_count = len(renamed.get("renamed", []) or [])
|
|
4557
|
+
if renamed_count:
|
|
4558
|
+
actions.append(f"personal-script-filenames-renamed:{renamed_count}")
|
|
4559
|
+
parts.append(f"{renamed_count} legacy personal filenames normalized")
|
|
4560
|
+
|
|
4347
4561
|
retired = reconcile_result.get("retired_superseded_scripts", {})
|
|
4348
4562
|
if isinstance(retired, dict):
|
|
4349
4563
|
archived = len(retired.get("archived", []) or [])
|
|
@@ -4440,7 +4654,7 @@ def manual_sync_update(
|
|
|
4440
4654
|
if copy_stats.get("script_conflicts"):
|
|
4441
4655
|
sync_result["actions"].append(f"preserved-personal-scripts:{len(copy_stats['script_conflicts'])}")
|
|
4442
4656
|
sync_result["warnings"].append(
|
|
4443
|
-
f"Preserved {len(copy_stats['script_conflicts'])} personal runtime script collision(s) in NEXO_HOME/scripts"
|
|
4657
|
+
f"Preserved {len(copy_stats['script_conflicts'])} personal runtime script collision(s) in NEXO_HOME/personal/scripts"
|
|
4444
4658
|
)
|
|
4445
4659
|
# Update runtime dependencies (best-effort)
|
|
4446
4660
|
try:
|