nexo-brain 7.1.1 → 7.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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 +232 -28
- package/src/autonomy_mandate.py +260 -0
- package/src/bootstrap_docs.py +22 -1
- package/src/cli.py +181 -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/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/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 +6 -10
- 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/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 +31 -23
- 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-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/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/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,7 @@ import threading
|
|
|
20
20
|
import time
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
|
+
from product_mode import enforce_desktop_product_contract
|
|
23
24
|
from runtime_home import export_resolved_nexo_home, managed_nexo_home
|
|
24
25
|
|
|
25
26
|
try:
|
|
@@ -87,6 +88,13 @@ _CORE_SCRIPT_RUNTIME_FILES = frozenset({
|
|
|
87
88
|
".watchdog-fails",
|
|
88
89
|
".watchdog-nexo-repair.lock",
|
|
89
90
|
})
|
|
91
|
+
_LEGACY_PERSONAL_BRAIN_DB_STUBS = frozenset({
|
|
92
|
+
"brain.db",
|
|
93
|
+
"cron_runs.db",
|
|
94
|
+
"nexo.db",
|
|
95
|
+
"nexo_brain.db",
|
|
96
|
+
"runtime.db",
|
|
97
|
+
})
|
|
90
98
|
|
|
91
99
|
|
|
92
100
|
def _log(msg: str):
|
|
@@ -188,6 +196,101 @@ def _cleanup_legacy_root_db_stubs(runtime_root: Path = NEXO_HOME, *, dry_run: bo
|
|
|
188
196
|
return report
|
|
189
197
|
|
|
190
198
|
|
|
199
|
+
def _sqlite_table_names(db_path: Path) -> list[str] | None:
|
|
200
|
+
"""Return user tables for a SQLite file or ``None`` when unreadable."""
|
|
201
|
+
import sqlite3
|
|
202
|
+
|
|
203
|
+
conn = None
|
|
204
|
+
try:
|
|
205
|
+
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
|
206
|
+
rows = conn.execute(
|
|
207
|
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
|
208
|
+
).fetchall()
|
|
209
|
+
return [str(row[0]) for row in rows if row and row[0]]
|
|
210
|
+
except Exception:
|
|
211
|
+
return None
|
|
212
|
+
finally:
|
|
213
|
+
if conn is not None:
|
|
214
|
+
try:
|
|
215
|
+
conn.close()
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _cleanup_empty_personal_brain_db_stubs(runtime_root: Path = NEXO_HOME, *, dry_run: bool = False) -> dict:
|
|
221
|
+
"""Archive obsolete DB shells from ``personal/brain`` after F0.6 migration.
|
|
222
|
+
|
|
223
|
+
Older layouts and partial migrations sometimes left duplicate DB files in
|
|
224
|
+
``personal/brain``. Post-F0.6 those DBs are not canonical anymore: the live
|
|
225
|
+
runtime DBs live under ``runtime/data``. This helper only archives files
|
|
226
|
+
from a known allowlist when they are clearly empty residue:
|
|
227
|
+
- zero-byte files, or
|
|
228
|
+
- SQLite shells with zero tables
|
|
229
|
+
|
|
230
|
+
Any file with real tables is left untouched.
|
|
231
|
+
"""
|
|
232
|
+
runtime_root = Path(runtime_root)
|
|
233
|
+
personal_brain_dir = runtime_root / "personal" / "brain"
|
|
234
|
+
report = {
|
|
235
|
+
"ok": True,
|
|
236
|
+
"dry_run": dry_run,
|
|
237
|
+
"candidates": [],
|
|
238
|
+
"archived": [],
|
|
239
|
+
"skipped": [],
|
|
240
|
+
"errors": [],
|
|
241
|
+
}
|
|
242
|
+
if not personal_brain_dir.is_dir():
|
|
243
|
+
return report
|
|
244
|
+
|
|
245
|
+
backup_root: Path | None = None
|
|
246
|
+
for name in sorted(_LEGACY_PERSONAL_BRAIN_DB_STUBS):
|
|
247
|
+
candidate = personal_brain_dir / name
|
|
248
|
+
if not candidate.is_file():
|
|
249
|
+
continue
|
|
250
|
+
try:
|
|
251
|
+
size = int(candidate.stat().st_size)
|
|
252
|
+
except OSError as exc:
|
|
253
|
+
report["errors"].append({"path": str(candidate), "error": str(exc)})
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
reason = ""
|
|
257
|
+
if size == 0:
|
|
258
|
+
reason = "zero-byte"
|
|
259
|
+
else:
|
|
260
|
+
tables = _sqlite_table_names(candidate)
|
|
261
|
+
if tables is None:
|
|
262
|
+
report["skipped"].append({"path": str(candidate), "reason": "uninspectable"})
|
|
263
|
+
continue
|
|
264
|
+
if tables:
|
|
265
|
+
report["skipped"].append({
|
|
266
|
+
"path": str(candidate),
|
|
267
|
+
"reason": "has-tables",
|
|
268
|
+
"table_count": len(tables),
|
|
269
|
+
})
|
|
270
|
+
continue
|
|
271
|
+
reason = "sqlite-empty-shell"
|
|
272
|
+
|
|
273
|
+
report["candidates"].append({"path": str(candidate), "size": size, "reason": reason})
|
|
274
|
+
if dry_run:
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
if backup_root is None:
|
|
278
|
+
timestamp = time.strftime("%Y-%m-%d-%H%M%S", time.gmtime())
|
|
279
|
+
backup_root = paths.backups_dir() / f"legacy-personal-brain-db-stubs-{timestamp}"
|
|
280
|
+
backup_root.mkdir(parents=True, exist_ok=True)
|
|
281
|
+
target = backup_root / candidate.name
|
|
282
|
+
try:
|
|
283
|
+
shutil.move(str(candidate), str(target))
|
|
284
|
+
report["archived"].append({
|
|
285
|
+
"path": str(candidate),
|
|
286
|
+
"backup_path": str(target),
|
|
287
|
+
"reason": reason,
|
|
288
|
+
})
|
|
289
|
+
except Exception as exc:
|
|
290
|
+
report["errors"].append({"path": str(candidate), "error": str(exc)})
|
|
291
|
+
return report
|
|
292
|
+
|
|
293
|
+
|
|
191
294
|
def _validate_db_backup(source_db: Path, backup_db: Path) -> dict:
|
|
192
295
|
"""Check that a backup preserves non-empty critical tables from the source DB."""
|
|
193
296
|
report = {
|
|
@@ -378,6 +481,7 @@ def _runtime_cli_wrapper_text() -> str:
|
|
|
378
481
|
'fi\n'
|
|
379
482
|
'NEXO_HOME="$RUNTIME_HOME"\n'
|
|
380
483
|
'export NEXO_HOME\n'
|
|
484
|
+
'export PYTHONDONTWRITEBYTECODE=1\n'
|
|
381
485
|
'resolve_code_dir() {\n'
|
|
382
486
|
' if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/cli.py" ]; then\n'
|
|
383
487
|
' printf \'%s\\n\' "${NEXO_CODE%/}"\n'
|
|
@@ -807,6 +911,17 @@ def _download_local_classifier_model() -> tuple[bool, str]:
|
|
|
807
911
|
return True, ""
|
|
808
912
|
|
|
809
913
|
|
|
914
|
+
def _local_classifier_install_command() -> list[str]:
|
|
915
|
+
in_virtualenv = bool(os.environ.get("VIRTUAL_ENV")) or (
|
|
916
|
+
getattr(sys, "prefix", "") != getattr(sys, "base_prefix", getattr(sys, "prefix", ""))
|
|
917
|
+
)
|
|
918
|
+
cmd = [sys.executable, "-m", "pip", "install"]
|
|
919
|
+
if not in_virtualenv:
|
|
920
|
+
cmd.append("--user")
|
|
921
|
+
cmd.extend(CLASSIFIER_INSTALL_PACKAGES)
|
|
922
|
+
return cmd
|
|
923
|
+
|
|
924
|
+
|
|
810
925
|
def _install_local_classifier_worker() -> None:
|
|
811
926
|
from classifier_local import MODEL_REVISION
|
|
812
927
|
|
|
@@ -832,7 +947,7 @@ def _install_local_classifier_worker() -> None:
|
|
|
832
947
|
})
|
|
833
948
|
return
|
|
834
949
|
|
|
835
|
-
install_cmd =
|
|
950
|
+
install_cmd = _local_classifier_install_command()
|
|
836
951
|
_write_classifier_install_log("[classifier-install] " + " ".join(install_cmd))
|
|
837
952
|
try:
|
|
838
953
|
result = subprocess.run(
|
|
@@ -1441,21 +1556,24 @@ def _migrate_effort_to_resonance(dest: Path = NEXO_HOME) -> list[str]:
|
|
|
1441
1556
|
|
|
1442
1557
|
def _relocate_resonance_tiers_contract(dest: Path = NEXO_HOME) -> list[str]:
|
|
1443
1558
|
"""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``.
|
|
1559
|
+
``NEXO_HOME/personal/brain/resonance_tiers.json`` and purge the legacy
|
|
1560
|
+
copy at ``NEXO_HOME/resonance_tiers.json``.
|
|
1446
1561
|
|
|
1447
1562
|
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
|
-
|
|
1563
|
+
``~/.nexo/brain/resonance_tiers.json`` but the F0.6 canonical location is
|
|
1564
|
+
``~/.nexo/personal/brain/resonance_tiers.json``. Older installers also
|
|
1565
|
+
copied the file to ``~/.nexo/resonance_tiers.json`` (legacy flat-file
|
|
1566
|
+
layout), so Desktop failed with *"NEXO Brain contract missing"* until the
|
|
1567
|
+
user moved the file by hand. The runtime now publishes to the canonical
|
|
1568
|
+
F0.6 brain dir while legacy ``~/.nexo/brain`` remains only a compatibility
|
|
1569
|
+
alias when present.
|
|
1570
|
+
|
|
1571
|
+
Idempotent: no-op once the contract file is in ``personal/brain`` and the
|
|
1572
|
+
legacy flat-file copy is gone. Never raises — migration must not block an
|
|
1573
|
+
update.
|
|
1456
1574
|
"""
|
|
1457
1575
|
actions: list[str] = []
|
|
1458
|
-
brain_dir = dest / "brain"
|
|
1576
|
+
brain_dir = dest / "personal" / "brain"
|
|
1459
1577
|
contract_path = brain_dir / "resonance_tiers.json"
|
|
1460
1578
|
legacy_path = dest / "resonance_tiers.json"
|
|
1461
1579
|
|
|
@@ -1465,7 +1583,8 @@ def _relocate_resonance_tiers_contract(dest: Path = NEXO_HOME) -> list[str]:
|
|
|
1465
1583
|
actions.append(f"resonance-contract-relocate-warning:mkdir:{exc.__class__.__name__}")
|
|
1466
1584
|
return actions
|
|
1467
1585
|
|
|
1468
|
-
# If the contract already exists in brain
|
|
1586
|
+
# If the contract already exists in the canonical brain dir, just drop the
|
|
1587
|
+
# legacy flat-file copy.
|
|
1469
1588
|
if contract_path.is_file():
|
|
1470
1589
|
if legacy_path.is_file():
|
|
1471
1590
|
try:
|
|
@@ -1475,7 +1594,8 @@ def _relocate_resonance_tiers_contract(dest: Path = NEXO_HOME) -> list[str]:
|
|
|
1475
1594
|
actions.append(f"resonance-contract-relocate-warning:unlink:{exc.__class__.__name__}")
|
|
1476
1595
|
return actions
|
|
1477
1596
|
|
|
1478
|
-
# Contract missing from brain
|
|
1597
|
+
# Contract missing from the canonical brain dir — promote the legacy file
|
|
1598
|
+
# if present.
|
|
1479
1599
|
if legacy_path.is_file():
|
|
1480
1600
|
try:
|
|
1481
1601
|
contract_path.write_bytes(legacy_path.read_bytes())
|
|
@@ -1960,7 +2080,6 @@ def _run_db_migrations() -> bool:
|
|
|
1960
2080
|
# personal_scripts.path + LaunchAgent plists, then write the
|
|
1961
2081
|
# F0.6 marker. Idempotent: returns immediately when already F0.6.
|
|
1962
2082
|
_maybe_migrate_to_f06_layout()
|
|
1963
|
-
_ensure_f06_legacy_shims()
|
|
1964
2083
|
try:
|
|
1965
2084
|
_rewrite_f06_launch_agents()
|
|
1966
2085
|
except Exception:
|
|
@@ -2128,6 +2247,49 @@ def _f06_packaged_code_file_targets() -> list[tuple[str, Path]]:
|
|
|
2128
2247
|
return targets
|
|
2129
2248
|
|
|
2130
2249
|
|
|
2250
|
+
def _f06_live_legacy_paths() -> list[Path]:
|
|
2251
|
+
"""Return real pre-F0.6 paths that still need migration.
|
|
2252
|
+
|
|
2253
|
+
Symlink shims do not count here. We only care about physical files or
|
|
2254
|
+
directories that keep the flat layout alive as a second source of truth.
|
|
2255
|
+
"""
|
|
2256
|
+
legacy_anchors = [
|
|
2257
|
+
NEXO_HOME / sub
|
|
2258
|
+
for sub in (
|
|
2259
|
+
"scripts",
|
|
2260
|
+
"brain",
|
|
2261
|
+
"data",
|
|
2262
|
+
"operations",
|
|
2263
|
+
"logs",
|
|
2264
|
+
"backups",
|
|
2265
|
+
"memory",
|
|
2266
|
+
"cognitive",
|
|
2267
|
+
"coordination",
|
|
2268
|
+
"exports",
|
|
2269
|
+
"nexo-email",
|
|
2270
|
+
"doctor",
|
|
2271
|
+
"snapshots",
|
|
2272
|
+
"crons",
|
|
2273
|
+
"skills",
|
|
2274
|
+
"plugins",
|
|
2275
|
+
"hooks",
|
|
2276
|
+
"rules",
|
|
2277
|
+
"db",
|
|
2278
|
+
"dashboard",
|
|
2279
|
+
"skills-core",
|
|
2280
|
+
)
|
|
2281
|
+
]
|
|
2282
|
+
present = [p for p in legacy_anchors if p.exists() and not p.is_symlink()]
|
|
2283
|
+
if present:
|
|
2284
|
+
return present
|
|
2285
|
+
live_files: list[Path] = []
|
|
2286
|
+
for legacy_name, _canonical in _f06_packaged_code_file_targets():
|
|
2287
|
+
candidate = NEXO_HOME / legacy_name
|
|
2288
|
+
if candidate.exists() and not candidate.is_symlink():
|
|
2289
|
+
live_files.append(candidate)
|
|
2290
|
+
return live_files
|
|
2291
|
+
|
|
2292
|
+
|
|
2131
2293
|
def _promote_packaged_runtime_code_to_core() -> None:
|
|
2132
2294
|
"""Move packaged runtime code out of ``~/.nexo`` root into ``core/``.
|
|
2133
2295
|
|
|
@@ -2424,6 +2586,23 @@ def _rewrite_f06_launch_agents() -> int:
|
|
|
2424
2586
|
return rewrites
|
|
2425
2587
|
|
|
2426
2588
|
|
|
2589
|
+
def _cleanup_f06_root_residue() -> None:
|
|
2590
|
+
"""Remove root-level cache artifacts that should not survive in F0.6 installs."""
|
|
2591
|
+
residue_targets = [
|
|
2592
|
+
NEXO_HOME / "__pycache__",
|
|
2593
|
+
]
|
|
2594
|
+
for target in residue_targets:
|
|
2595
|
+
try:
|
|
2596
|
+
if not target.exists() and not target.is_symlink():
|
|
2597
|
+
continue
|
|
2598
|
+
if target.is_symlink() or target.is_file():
|
|
2599
|
+
target.unlink(missing_ok=True)
|
|
2600
|
+
continue
|
|
2601
|
+
shutil.rmtree(target, ignore_errors=True)
|
|
2602
|
+
except Exception as exc:
|
|
2603
|
+
_log(f"[F0.6] residue cleanup skipped for {target}: {exc}")
|
|
2604
|
+
|
|
2605
|
+
|
|
2427
2606
|
def _maybe_migrate_to_f06_layout() -> None:
|
|
2428
2607
|
"""Plan F0.6 — one-shot physical layout migration. Idempotent.
|
|
2429
2608
|
|
|
@@ -2449,7 +2628,9 @@ def _maybe_migrate_to_f06_layout() -> None:
|
|
|
2449
2628
|
current = ""
|
|
2450
2629
|
if current == "F0.6":
|
|
2451
2630
|
_promote_packaged_runtime_code_to_core()
|
|
2452
|
-
|
|
2631
|
+
if _f06_live_legacy_paths():
|
|
2632
|
+
_ensure_f06_legacy_shims()
|
|
2633
|
+
_cleanup_f06_root_residue()
|
|
2453
2634
|
try:
|
|
2454
2635
|
_rewrite_f06_launch_agents()
|
|
2455
2636
|
except Exception:
|
|
@@ -2457,14 +2638,7 @@ def _maybe_migrate_to_f06_layout() -> None:
|
|
|
2457
2638
|
return # already migrated
|
|
2458
2639
|
# Refuse to migrate from a non-NEXO_HOME or a fresh install
|
|
2459
2640
|
# 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()]
|
|
2641
|
+
present = _f06_live_legacy_paths()
|
|
2468
2642
|
if not present:
|
|
2469
2643
|
# Fresh install — write marker and return.
|
|
2470
2644
|
try:
|
|
@@ -2671,6 +2845,7 @@ def _maybe_migrate_to_f06_layout() -> None:
|
|
|
2671
2845
|
except Exception as e:
|
|
2672
2846
|
_log(f"[F0.6] marker write failed: {e}")
|
|
2673
2847
|
_ensure_f06_legacy_shims()
|
|
2848
|
+
_cleanup_f06_root_residue()
|
|
2674
2849
|
try:
|
|
2675
2850
|
rewritten = _rewrite_f06_launch_agents()
|
|
2676
2851
|
if rewritten:
|
|
@@ -3407,7 +3582,14 @@ def _auto_update_check_locked() -> dict:
|
|
|
3407
3582
|
except Exception as e:
|
|
3408
3583
|
_log(f"evolution-objective.json backfill error: {e}")
|
|
3409
3584
|
|
|
3410
|
-
|
|
3585
|
+
try:
|
|
3586
|
+
desktop_contract = enforce_desktop_product_contract(source="auto_update")
|
|
3587
|
+
if desktop_contract.get("applied") and desktop_contract.get("changed_objective"):
|
|
3588
|
+
_log("Desktop product contract enforced: evolution disabled")
|
|
3589
|
+
except Exception as e:
|
|
3590
|
+
_log(f"desktop product contract error: {e}")
|
|
3591
|
+
|
|
3592
|
+
# Backfill installed core/scripts/ for existing installs
|
|
3411
3593
|
try:
|
|
3412
3594
|
scripts_dest = paths.core_scripts_dir()
|
|
3413
3595
|
# Deduce NEXO_CODE: env var first, then from __file__ (auto_update.py is in src/)
|
|
@@ -3422,7 +3604,7 @@ def _auto_update_check_locked() -> dict:
|
|
|
3422
3604
|
dest = scripts_dest / f.name
|
|
3423
3605
|
if f.is_file() and not dest.exists():
|
|
3424
3606
|
shutil.copy2(str(f), str(dest))
|
|
3425
|
-
_log("Backfilled NEXO_HOME/scripts
|
|
3607
|
+
_log("Backfilled NEXO_HOME/core/scripts from repo scripts for existing install")
|
|
3426
3608
|
except Exception as e:
|
|
3427
3609
|
_log(f"scripts backfill error: {e}")
|
|
3428
3610
|
|
|
@@ -4086,7 +4268,6 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
|
|
|
4086
4268
|
try:
|
|
4087
4269
|
_emit_progress(progress_fn, "Canonicalizing packaged runtime layout...")
|
|
4088
4270
|
_maybe_migrate_to_f06_layout()
|
|
4089
|
-
_ensure_f06_legacy_shims()
|
|
4090
4271
|
try:
|
|
4091
4272
|
_rewrite_f06_launch_agents()
|
|
4092
4273
|
except Exception:
|
|
@@ -4103,6 +4284,22 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
|
|
|
4103
4284
|
except Exception as exc:
|
|
4104
4285
|
actions.append(f"legacy-root-db-stubs-warning:{exc.__class__.__name__}")
|
|
4105
4286
|
|
|
4287
|
+
try:
|
|
4288
|
+
personal_brain_cleanup = _cleanup_empty_personal_brain_db_stubs(dest, dry_run=False)
|
|
4289
|
+
archived = len(personal_brain_cleanup.get("archived", []) or [])
|
|
4290
|
+
if archived:
|
|
4291
|
+
actions.append(f"legacy-personal-brain-db-stubs-archived:{archived}")
|
|
4292
|
+
except Exception as exc:
|
|
4293
|
+
actions.append(f"legacy-personal-brain-db-stubs-warning:{exc.__class__.__name__}")
|
|
4294
|
+
|
|
4295
|
+
try:
|
|
4296
|
+
_emit_progress(progress_fn, "Applying Desktop product contract...")
|
|
4297
|
+
contract = enforce_desktop_product_contract(source="runtime_post_sync")
|
|
4298
|
+
if contract.get("applied"):
|
|
4299
|
+
actions.append("desktop-product-contract")
|
|
4300
|
+
except Exception as exc:
|
|
4301
|
+
actions.append(f"desktop-product-contract-warning:{exc.__class__.__name__}")
|
|
4302
|
+
|
|
4106
4303
|
code_root = _runtime_code_dir(dest)
|
|
4107
4304
|
env = {**os.environ, "NEXO_HOME": str(dest), "NEXO_CODE": str(code_root)}
|
|
4108
4305
|
|
|
@@ -4344,6 +4541,13 @@ def _personal_schedule_reconcile_summary(reconcile_result: dict) -> tuple[list[s
|
|
|
4344
4541
|
actions: list[str] = []
|
|
4345
4542
|
parts: list[str] = []
|
|
4346
4543
|
|
|
4544
|
+
renamed = reconcile_result.get("renamed_legacy_filenames", {})
|
|
4545
|
+
if isinstance(renamed, dict):
|
|
4546
|
+
renamed_count = len(renamed.get("renamed", []) or [])
|
|
4547
|
+
if renamed_count:
|
|
4548
|
+
actions.append(f"personal-script-filenames-renamed:{renamed_count}")
|
|
4549
|
+
parts.append(f"{renamed_count} legacy personal filenames normalized")
|
|
4550
|
+
|
|
4347
4551
|
retired = reconcile_result.get("retired_superseded_scripts", {})
|
|
4348
4552
|
if isinstance(retired, dict):
|
|
4349
4553
|
archived = len(retired.get("archived", []) or [])
|
|
@@ -4440,7 +4644,7 @@ def manual_sync_update(
|
|
|
4440
4644
|
if copy_stats.get("script_conflicts"):
|
|
4441
4645
|
sync_result["actions"].append(f"preserved-personal-scripts:{len(copy_stats['script_conflicts'])}")
|
|
4442
4646
|
sync_result["warnings"].append(
|
|
4443
|
-
f"Preserved {len(copy_stats['script_conflicts'])} personal runtime script collision(s) in NEXO_HOME/scripts"
|
|
4647
|
+
f"Preserved {len(copy_stats['script_conflicts'])} personal runtime script collision(s) in NEXO_HOME/personal/scripts"
|
|
4444
4648
|
)
|
|
4445
4649
|
# Update runtime dependencies (best-effort)
|
|
4446
4650
|
try:
|