nexo-brain 7.1.0 → 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 +305 -42
- 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
|
|
|
@@ -3612,6 +3794,23 @@ def _resolve_sync_source() -> tuple[Path | None, Path | None]:
|
|
|
3612
3794
|
return candidate
|
|
3613
3795
|
return None
|
|
3614
3796
|
|
|
3797
|
+
try:
|
|
3798
|
+
runtime_core = (dest / "core").resolve()
|
|
3799
|
+
code_resolved = NEXO_CODE.resolve()
|
|
3800
|
+
except Exception:
|
|
3801
|
+
runtime_core = dest / "core"
|
|
3802
|
+
code_resolved = NEXO_CODE
|
|
3803
|
+
|
|
3804
|
+
# Packaged/runtime-only installs resolve the launcher to ``~/.nexo/core``.
|
|
3805
|
+
# Those must use the packaged updater path instead of treating the managed
|
|
3806
|
+
# runtime itself as a mutable source repository. Only a recorded external
|
|
3807
|
+
# source repo in ``version.json`` should reactivate source-sync mode.
|
|
3808
|
+
if code_resolved == runtime_core:
|
|
3809
|
+
version_source = _runtime_version_source()
|
|
3810
|
+
if version_source:
|
|
3811
|
+
return version_source / "src", version_source
|
|
3812
|
+
return None, None
|
|
3813
|
+
|
|
3615
3814
|
try:
|
|
3616
3815
|
same_as_runtime = NEXO_CODE.resolve() == dest.resolve()
|
|
3617
3816
|
except Exception:
|
|
@@ -3798,6 +3997,31 @@ def _backup_runtime_tree(dest: Path = NEXO_HOME) -> str:
|
|
|
3798
3997
|
return str(backup_dir)
|
|
3799
3998
|
|
|
3800
3999
|
|
|
4000
|
+
def _remove_runtime_copy_target(target: Path) -> None:
|
|
4001
|
+
"""Remove a runtime copy destination, handling symlinks explicitly.
|
|
4002
|
+
|
|
4003
|
+
F0.6 installs keep compatibility shims such as ``~/.nexo/db ->
|
|
4004
|
+
~/.nexo/core/db``. ``shutil.rmtree(..., ignore_errors=True)`` does not
|
|
4005
|
+
remove those symlinks, which makes subsequent ``copytree(...)`` calls fail
|
|
4006
|
+
with ``FileExistsError`` during ``nexo update``. This helper treats
|
|
4007
|
+
symlink/file targets differently from real directories so runtime sync and
|
|
4008
|
+
rollback can replace shim paths safely.
|
|
4009
|
+
"""
|
|
4010
|
+
import shutil
|
|
4011
|
+
|
|
4012
|
+
try:
|
|
4013
|
+
if target.is_symlink() or target.is_file():
|
|
4014
|
+
target.unlink()
|
|
4015
|
+
return
|
|
4016
|
+
if target.is_dir():
|
|
4017
|
+
shutil.rmtree(str(target), ignore_errors=True)
|
|
4018
|
+
return
|
|
4019
|
+
if target.exists():
|
|
4020
|
+
target.unlink()
|
|
4021
|
+
except FileNotFoundError:
|
|
4022
|
+
return
|
|
4023
|
+
|
|
4024
|
+
|
|
3801
4025
|
def _restore_runtime_tree(backup_dir: str, dest: Path = NEXO_HOME) -> None:
|
|
3802
4026
|
import shutil
|
|
3803
4027
|
|
|
@@ -3807,11 +4031,11 @@ def _restore_runtime_tree(backup_dir: str, dest: Path = NEXO_HOME) -> None:
|
|
|
3807
4031
|
for item in bdir.iterdir():
|
|
3808
4032
|
target = dest / item.name
|
|
3809
4033
|
if item.is_dir():
|
|
3810
|
-
|
|
3811
|
-
shutil.rmtree(target, ignore_errors=True)
|
|
4034
|
+
_remove_runtime_copy_target(target)
|
|
3812
4035
|
shutil.copytree(str(item), str(target))
|
|
3813
4036
|
else:
|
|
3814
4037
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
4038
|
+
_remove_runtime_copy_target(target)
|
|
3815
4039
|
shutil.copy2(str(item), str(target))
|
|
3816
4040
|
|
|
3817
4041
|
|
|
@@ -3834,8 +4058,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
3834
4058
|
pkg_src = src_dir / pkg
|
|
3835
4059
|
pkg_dest = dest / pkg
|
|
3836
4060
|
if pkg_src.is_dir():
|
|
3837
|
-
|
|
3838
|
-
shutil.rmtree(str(pkg_dest), ignore_errors=True)
|
|
4061
|
+
_remove_runtime_copy_target(pkg_dest)
|
|
3839
4062
|
shutil.copytree(
|
|
3840
4063
|
str(pkg_src),
|
|
3841
4064
|
str(pkg_dest),
|
|
@@ -3847,30 +4070,37 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
3847
4070
|
for name in flat_files:
|
|
3848
4071
|
src_file = src_dir / name
|
|
3849
4072
|
if src_file.is_file():
|
|
3850
|
-
|
|
4073
|
+
target = dest / name
|
|
4074
|
+
_remove_runtime_copy_target(target)
|
|
4075
|
+
shutil.copy2(str(src_file), str(target))
|
|
3851
4076
|
copied_files += 1
|
|
3852
4077
|
|
|
3853
4078
|
_emit_progress(progress_fn, "Copying plugin modules...")
|
|
3854
4079
|
plugins_src = src_dir / "plugins"
|
|
3855
4080
|
plugins_dest = dest / "plugins"
|
|
3856
4081
|
if plugins_src.is_dir():
|
|
4082
|
+
if plugins_dest.is_symlink():
|
|
4083
|
+
_remove_runtime_copy_target(plugins_dest)
|
|
3857
4084
|
plugins_dest.mkdir(parents=True, exist_ok=True)
|
|
3858
4085
|
for item in plugins_src.iterdir():
|
|
3859
4086
|
if item.is_file() and item.suffix == ".py" and not is_duplicate_artifact_name(item):
|
|
3860
|
-
|
|
4087
|
+
target = plugins_dest / item.name
|
|
4088
|
+
_remove_runtime_copy_target(target)
|
|
4089
|
+
shutil.copy2(str(item), str(target))
|
|
3861
4090
|
|
|
3862
4091
|
_emit_progress(progress_fn, "Copying scripts...")
|
|
3863
4092
|
scripts_src = src_dir / "scripts"
|
|
3864
4093
|
scripts_dest = dest / "scripts"
|
|
3865
4094
|
if scripts_src.is_dir():
|
|
4095
|
+
if scripts_dest.is_symlink():
|
|
4096
|
+
_remove_runtime_copy_target(scripts_dest)
|
|
3866
4097
|
scripts_dest.mkdir(parents=True, exist_ok=True)
|
|
3867
4098
|
for item in scripts_src.iterdir():
|
|
3868
4099
|
if item.name == "__pycache__" or item.name.startswith(".") or is_duplicate_artifact_name(item):
|
|
3869
4100
|
continue
|
|
3870
4101
|
dst = scripts_dest / item.name
|
|
3871
4102
|
if item.is_dir():
|
|
3872
|
-
|
|
3873
|
-
shutil.rmtree(str(dst), ignore_errors=True)
|
|
4103
|
+
_remove_runtime_copy_target(dst)
|
|
3874
4104
|
shutil.copytree(str(item), str(dst), ignore=_runtime_copy_ignore())
|
|
3875
4105
|
elif item.is_file():
|
|
3876
4106
|
existing_class = installed_script_classes.get(item.name, "")
|
|
@@ -3899,25 +4129,37 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
3899
4129
|
templates_src = repo_dir / "templates"
|
|
3900
4130
|
templates_dest = dest / "templates"
|
|
3901
4131
|
if templates_src.is_dir():
|
|
4132
|
+
if templates_dest.is_symlink():
|
|
4133
|
+
_remove_runtime_copy_target(templates_dest)
|
|
3902
4134
|
templates_dest.mkdir(parents=True, exist_ok=True)
|
|
3903
4135
|
for item in templates_src.iterdir():
|
|
3904
4136
|
if item.name == "__pycache__" or is_duplicate_artifact_name(item):
|
|
3905
4137
|
continue
|
|
3906
4138
|
if item.is_file():
|
|
3907
|
-
|
|
4139
|
+
target = templates_dest / item.name
|
|
4140
|
+
_remove_runtime_copy_target(target)
|
|
4141
|
+
shutil.copy2(str(item), str(target))
|
|
3908
4142
|
elif item.is_dir():
|
|
3909
4143
|
sub_dest = templates_dest / item.name
|
|
4144
|
+
if sub_dest.is_symlink():
|
|
4145
|
+
_remove_runtime_copy_target(sub_dest)
|
|
3910
4146
|
sub_dest.mkdir(parents=True, exist_ok=True)
|
|
3911
4147
|
for sub in item.iterdir():
|
|
3912
4148
|
if sub.is_file() and not is_duplicate_artifact_name(sub):
|
|
3913
|
-
|
|
4149
|
+
target = sub_dest / sub.name
|
|
4150
|
+
_remove_runtime_copy_target(target)
|
|
4151
|
+
shutil.copy2(str(sub), str(target))
|
|
3914
4152
|
|
|
3915
4153
|
package_json = repo_dir / "package.json"
|
|
3916
4154
|
if package_json.is_file():
|
|
3917
|
-
|
|
4155
|
+
package_target = dest / "package.json"
|
|
4156
|
+
_remove_runtime_copy_target(package_target)
|
|
4157
|
+
shutil.copy2(str(package_json), str(package_target))
|
|
3918
4158
|
try:
|
|
3919
4159
|
pkg = json.loads(package_json.read_text())
|
|
3920
|
-
|
|
4160
|
+
version_target = dest / "version.json"
|
|
4161
|
+
_remove_runtime_copy_target(version_target)
|
|
4162
|
+
version_target.write_text(json.dumps({
|
|
3921
4163
|
"version": pkg.get("version", "?"),
|
|
3922
4164
|
"source": str(repo_dir),
|
|
3923
4165
|
}, indent=2))
|
|
@@ -3928,8 +4170,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
3928
4170
|
skills_src = src_dir / "skills"
|
|
3929
4171
|
skills_dest = dest / "skills-core"
|
|
3930
4172
|
if skills_src.is_dir():
|
|
3931
|
-
|
|
3932
|
-
shutil.rmtree(str(skills_dest), ignore_errors=True)
|
|
4173
|
+
_remove_runtime_copy_target(skills_dest)
|
|
3933
4174
|
shutil.copytree(str(skills_src), str(skills_dest), ignore=_runtime_copy_ignore())
|
|
3934
4175
|
|
|
3935
4176
|
bin_dir = dest / "bin"
|
|
@@ -4027,7 +4268,6 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
|
|
|
4027
4268
|
try:
|
|
4028
4269
|
_emit_progress(progress_fn, "Canonicalizing packaged runtime layout...")
|
|
4029
4270
|
_maybe_migrate_to_f06_layout()
|
|
4030
|
-
_ensure_f06_legacy_shims()
|
|
4031
4271
|
try:
|
|
4032
4272
|
_rewrite_f06_launch_agents()
|
|
4033
4273
|
except Exception:
|
|
@@ -4044,6 +4284,22 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
|
|
|
4044
4284
|
except Exception as exc:
|
|
4045
4285
|
actions.append(f"legacy-root-db-stubs-warning:{exc.__class__.__name__}")
|
|
4046
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
|
+
|
|
4047
4303
|
code_root = _runtime_code_dir(dest)
|
|
4048
4304
|
env = {**os.environ, "NEXO_HOME": str(dest), "NEXO_CODE": str(code_root)}
|
|
4049
4305
|
|
|
@@ -4285,6 +4541,13 @@ def _personal_schedule_reconcile_summary(reconcile_result: dict) -> tuple[list[s
|
|
|
4285
4541
|
actions: list[str] = []
|
|
4286
4542
|
parts: list[str] = []
|
|
4287
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
|
+
|
|
4288
4551
|
retired = reconcile_result.get("retired_superseded_scripts", {})
|
|
4289
4552
|
if isinstance(retired, dict):
|
|
4290
4553
|
archived = len(retired.get("archived", []) or [])
|
|
@@ -4381,7 +4644,7 @@ def manual_sync_update(
|
|
|
4381
4644
|
if copy_stats.get("script_conflicts"):
|
|
4382
4645
|
sync_result["actions"].append(f"preserved-personal-scripts:{len(copy_stats['script_conflicts'])}")
|
|
4383
4646
|
sync_result["warnings"].append(
|
|
4384
|
-
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"
|
|
4385
4648
|
)
|
|
4386
4649
|
# Update runtime dependencies (best-effort)
|
|
4387
4650
|
try:
|