nexo-brain 7.9.8 → 7.9.10

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.9.8",
3
+ "version": "7.9.10",
4
4
  "description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
5
5
  "author": {
6
6
  "name": "NEXO Brain",
package/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  [Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
20
20
 
21
- Version `7.9.8` is the current packaged-runtime line. Hotfix release over `7.9.7`: packaged installs that already run from `~/.nexo/core/current -> versions/<version>` no longer mistake that managed snapshot for a mutable source checkout during `nexo update`. The updater now stays on the packaged path unless `version.json` points to a real external repo, so installed users stop “updating” from the old runtime back into itself. Coordinated Desktop v0.28.10 bundles the same fix.
21
+ Version `7.9.10` is the current packaged-runtime line. Hotfix release over `7.9.9`: `nexo_doctor` now defaults blank calls to `runtime_personal` instead of failing on missing `plane`, and non-interactive installer/update paths preserve existing identity defaults from runtime profile/calibration metadata instead of blindly rewriting operators to generic onboarding defaults. Desktop remains at v0.28.11 because the extra fixes are Brain-only.
22
22
 
23
23
  Previously in `7.9.5`: patch release that fixes canonical diary confirmation for Desktop: Brain resolves the Desktop/Claude session UUID through NEXO SID aliases before checking `session_diary`, so archive/delete/app-exit can confirm diaries written by `nexo_session_diary_write` under the active `nexo-...` SID. Verification: `pytest tests/test_lifecycle_events.py` (28 passing) plus coordinated Desktop v0.28.6 shutdown/archive/delete/app-exit checks.
24
24
 
package/bin/nexo-brain.js CHANGED
@@ -252,6 +252,24 @@ function readRuntimeCalibration(nexoHome = NEXO_HOME) {
252
252
  return { path: null, payload: null };
253
253
  }
254
254
 
255
+ function profilePathCandidates(nexoHome = NEXO_HOME) {
256
+ return [
257
+ path.join(nexoHome, "personal", "brain", "profile.json"),
258
+ path.join(nexoHome, "brain", "profile.json"),
259
+ ];
260
+ }
261
+
262
+ function readRuntimeProfile(nexoHome = NEXO_HOME) {
263
+ for (const filePath of profilePathCandidates(nexoHome)) {
264
+ if (!fs.existsSync(filePath)) continue;
265
+ const payload = readJsonFile(filePath);
266
+ if (payload && typeof payload === "object") {
267
+ return { path: filePath, payload };
268
+ }
269
+ }
270
+ return { path: null, payload: null };
271
+ }
272
+
255
273
  function nonEmptyString(value) {
256
274
  return typeof value === "string" && value.trim().length > 0;
257
275
  }
@@ -288,6 +306,55 @@ function hasPartialPlaceholderCalibration(calibration) {
288
306
  return isPlaceholderUserName(name) || !nonEmptyString(language);
289
307
  }
290
308
 
309
+ function firstMeaningfulString(...values) {
310
+ for (const value of values) {
311
+ if (typeof value !== "string") continue;
312
+ const clean = value.trim();
313
+ if (clean) return clean;
314
+ }
315
+ return "";
316
+ }
317
+
318
+ function normalizeLanguageCode(value) {
319
+ const clean = String(value || "").trim().toLowerCase().replace("_", "-");
320
+ if (!clean) return "";
321
+ return clean.split("-")[0];
322
+ }
323
+
324
+ function resolveExistingIdentityDefaults(nexoHome = NEXO_HOME) {
325
+ const calibration = readRuntimeCalibration(nexoHome).payload || {};
326
+ const user = calibration.user && typeof calibration.user === "object" ? calibration.user : {};
327
+ const profile = readRuntimeProfile(nexoHome).payload || {};
328
+ const version = readJsonFile(path.join(nexoHome, "version.json")) || {};
329
+
330
+ const userName = firstMeaningfulString(
331
+ user.name,
332
+ calibration.user_name,
333
+ profile.user_name,
334
+ version.user_name,
335
+ );
336
+ const language = normalizeLanguageCode(firstMeaningfulString(
337
+ user.language,
338
+ calibration.language,
339
+ profile.language,
340
+ version.language,
341
+ ));
342
+ const operatorName = firstMeaningfulString(
343
+ user.assistant_name,
344
+ calibration.assistant_name,
345
+ calibration.operator_name,
346
+ profile.assistant_name,
347
+ profile.operator_name,
348
+ version.operator_name,
349
+ );
350
+
351
+ return {
352
+ userName: !isPlaceholderUserName(userName) ? userName : "",
353
+ language,
354
+ operatorName: !isReservedAssistantName(operatorName) ? operatorName : "",
355
+ };
356
+ }
357
+
291
358
  function ensureOnboardingCompletionMarker(nexoHome = NEXO_HOME) {
292
359
  const record = readRuntimeCalibration(nexoHome);
293
360
  if (!record.path || !isOnboardingComplete(record.payload)) {
@@ -684,6 +751,59 @@ function finalizeF06Layout(python, nexoHome = NEXO_HOME) {
684
751
  }
685
752
  }
686
753
 
754
+ function readRuntimeVersionFrom(basePath) {
755
+ if (!basePath) return "";
756
+ for (const candidate of [
757
+ path.join(basePath, "version.json"),
758
+ path.join(basePath, "package.json"),
759
+ ]) {
760
+ try {
761
+ if (!fs.existsSync(candidate)) continue;
762
+ const payload = JSON.parse(fs.readFileSync(candidate, "utf8"));
763
+ const version = String(payload.version || "").trim();
764
+ if (version) return version;
765
+ } catch (_) {}
766
+ }
767
+ return "";
768
+ }
769
+
770
+ function readActiveRuntimeSnapshotVersion(nexoHome = NEXO_HOME) {
771
+ return readRuntimeVersionFrom(path.join(nexoHome, "core", "current"));
772
+ }
773
+
774
+ function activateVersionedRuntimeSnapshot(python, nexoHome = NEXO_HOME, version = "") {
775
+ try {
776
+ const srcDir = path.join(__dirname, "..", "src");
777
+ const inline = [
778
+ "import json, os, pathlib, sys",
779
+ `sys.path.insert(0, ${JSON.stringify(srcDir)})`,
780
+ "from runtime_versioning import activate_versioned_runtime_snapshot",
781
+ "home = pathlib.Path(os.environ['NEXO_HOME'])",
782
+ `result = activate_versioned_runtime_snapshot(source_root=home / 'core', version=${JSON.stringify(version)})`,
783
+ "print(json.dumps(result))",
784
+ ].join("; ");
785
+ const result = spawnSync(python, ["-c", inline], {
786
+ cwd: nexoHome,
787
+ env: {
788
+ ...process.env,
789
+ NEXO_HOME: nexoHome,
790
+ },
791
+ encoding: "utf8",
792
+ });
793
+ if (result.status !== 0) {
794
+ const detail = (result.stderr || result.stdout || "").trim();
795
+ throw new Error(detail || "activation command failed");
796
+ }
797
+ const payload = JSON.parse(String(result.stdout || "{}").trim() || "{}");
798
+ if (!payload || payload.ok !== true) {
799
+ throw new Error(payload && payload.error ? payload.error : "activation returned not-ok");
800
+ }
801
+ return payload;
802
+ } catch (err) {
803
+ return { ok: false, error: String((err && err.message) || err) };
804
+ }
805
+ }
806
+
687
807
  function getCoreRuntimeFlatFiles(srcDir = path.join(__dirname, "..", "src")) {
688
808
  const staticFiles = [
689
809
  "server.py",
@@ -2296,10 +2416,17 @@ async function runSetup() {
2296
2416
  const currentPkg = readPackageJson();
2297
2417
  const installedVersion = installed.version || "0.0.0";
2298
2418
  const currentVersion = currentPkg.version;
2299
-
2300
- if (installedVersion !== currentVersion) {
2301
- log(`Existing installation detected: v${installedVersion} → v${currentVersion}`);
2302
- log("Running auto-migration...");
2419
+ const activeRuntimeVersion = readActiveRuntimeSnapshotVersion(NEXO_HOME);
2420
+ const needsRuntimeRepair = activeRuntimeVersion !== currentVersion;
2421
+
2422
+ if (installedVersion !== currentVersion || needsRuntimeRepair) {
2423
+ if (installedVersion !== currentVersion) {
2424
+ log(`Existing installation detected: v${installedVersion} → v${currentVersion}`);
2425
+ log("Running auto-migration...");
2426
+ } else {
2427
+ log(`Existing installation detected: metadata v${installedVersion}, runtime core/current v${activeRuntimeVersion || "missing"}`);
2428
+ log("Repairing active runtime snapshot...");
2429
+ }
2303
2430
 
2304
2431
  // Recursive copy helper (skips __pycache__, .pyc, .db files)
2305
2432
  const srcDir = path.join(__dirname, "..", "src");
@@ -2469,6 +2596,9 @@ async function runSetup() {
2469
2596
  installed_at: installed.installed_at,
2470
2597
  updated_at: new Date().toISOString(),
2471
2598
  migrated_from: installedVersion,
2599
+ ...(installedVersion === currentVersion && activeRuntimeVersion && activeRuntimeVersion !== currentVersion
2600
+ ? { runtime_repaired_from: activeRuntimeVersion }
2601
+ : {}),
2472
2602
  }, null, 2));
2473
2603
  syncRuntimePackageMetadata(path.join(__dirname, ".."), NEXO_HOME);
2474
2604
  log("Finalizing F0.6 runtime layout...");
@@ -2476,6 +2606,11 @@ async function runSetup() {
2476
2606
  if (!migLayoutFinalize.ok) {
2477
2607
  throw new Error(`F0.6 layout finalization failed: ${migLayoutFinalize.error}`);
2478
2608
  }
2609
+ const migActivation = activateVersionedRuntimeSnapshot(migPython, NEXO_HOME, currentVersion);
2610
+ if (!migActivation.ok) {
2611
+ throw new Error(`Runtime activation failed: ${migActivation.error}`);
2612
+ }
2613
+ log(` Runtime activation: core/current -> versions/${currentVersion}`);
2479
2614
 
2480
2615
  // Keep the rendered template in-memory for version tracking, but do
2481
2616
  // not drop a loose reference file in NEXO_HOME root.
@@ -2898,9 +3033,11 @@ async function runSetup() {
2898
3033
  },
2899
3034
  };
2900
3035
 
3036
+ const existingIdentity = resolveExistingIdentityDefaults(NEXO_HOME);
3037
+
2901
3038
  // Detect language from input or use default
2902
- let lang = "en";
2903
- let t = i18n.en;
3039
+ let lang = existingIdentity.language || "en";
3040
+ let t = i18n[lang] || i18n.en;
2904
3041
  if (!useDefaults) {
2905
3042
  const langInput = await ask(" What's your preferred language? / ¿En qué idioma prefieres hablar?\n > ");
2906
3043
  const langLower = langInput.trim().toLowerCase();
@@ -2941,7 +3078,7 @@ async function runSetup() {
2941
3078
  // Step 2: User's name (P2) — v6.0.0 empty input falls through to "Usuario"
2942
3079
  // instead of keeping an empty string. The calibration file always ships
2943
3080
  // with a concrete user.name so downstream tooling does not need guards.
2944
- let userName = "Usuario";
3081
+ let userName = existingIdentity.userName || "Usuario";
2945
3082
  if (!useDefaults) {
2946
3083
  const nameInput = await ask(t.askUserName);
2947
3084
  const trimmedName = nameInput.trim();
@@ -2953,7 +3090,7 @@ async function runSetup() {
2953
3090
  }
2954
3091
 
2955
3092
  // Step 3: Agent name (P3)
2956
- let operatorName = DEFAULT_ASSISTANT_NAME;
3093
+ let operatorName = existingIdentity.operatorName || DEFAULT_ASSISTANT_NAME;
2957
3094
  if (!useDefaults) {
2958
3095
  while (true) {
2959
3096
  const name = await ask(t.askAgentName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.9.8",
3
+ "version": "7.9.10",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
6
6
  "homepage": "https://nexo-brain.com",
@@ -28,6 +28,7 @@ VALID_DIAGNOSTIC_PLANES = {
28
28
  }
29
29
 
30
30
  DOCTOR_COMPATIBLE_PLANES = {"runtime_personal", "installation_live", "database_real"}
31
+ DEFAULT_DOCTOR_PLANE = "runtime_personal"
31
32
 
32
33
 
33
34
  def normalize_diagnostic_plane(plane: str = "") -> str:
@@ -40,15 +41,19 @@ def diagnostic_plane_choices() -> list[str]:
40
41
 
41
42
 
42
43
  def diagnostic_plane_preflight(plane: str = "") -> tuple[str, DoctorCheck | None]:
43
- clean_plane = normalize_diagnostic_plane(plane)
44
+ raw_plane = str(plane or "").strip()
45
+ if not raw_plane:
46
+ return DEFAULT_DOCTOR_PLANE, None
47
+
48
+ clean_plane = normalize_diagnostic_plane(raw_plane)
44
49
  if not clean_plane:
45
50
  options = ", ".join(diagnostic_plane_choices())
46
51
  return "", DoctorCheck(
47
- id="orchestrator.diagnostic_plane_required",
52
+ id="orchestrator.diagnostic_plane_invalid",
48
53
  tier="orchestrator",
49
54
  status="critical",
50
55
  severity="error",
51
- summary="El diagnóstico está bloqueado hasta fijar explícitamente el plano",
56
+ summary=f"Plano diagnóstico desconocido: {raw_plane}",
52
57
  evidence=[
53
58
  f"planes válidos: {options}",
54
59
  "Usa `runtime_personal` para ~/.nexo y hábitos del runtime; `installation_live` para hooks/clientes/instalación; `database_real` para filas y schema reales.",
@@ -58,8 +63,7 @@ def diagnostic_plane_preflight(plane: str = "") -> tuple[str, DoctorCheck | None
58
63
  "Si el problema pertenece a producto público o al co-operador, usa el surface correcto en vez de NEXO Doctor.",
59
64
  ],
60
65
  escalation_prompt=(
61
- "NEXO mezcló planos en diagnósticos anteriores. El doctor no debe correr hasta que se elija "
62
- "explícitamente si el problema está en producto público, runtime personal, instalación viva, BD real o co-operador."
66
+ "El plano elegido no existe. Repite el diagnóstico con un plano válido para evitar mezclar runtime, instalación, BD real o surfaces ajenas al doctor."
63
67
  ),
64
68
  )
65
69
 
@@ -22,26 +22,11 @@ def handle_doctor(tier: str = "boot", fix: bool = False, output: str = "text", p
22
22
  """
23
23
  from doctor.orchestrator import run_doctor
24
24
  from doctor.formatters import format_report
25
- from doctor.planes import diagnostic_plane_choices
26
25
 
27
26
  if tier not in ("boot", "runtime", "deep", "all"):
28
27
  return f"Invalid tier '{tier}'. Use: boot, runtime, deep, all"
29
28
  if output not in ("text", "json"):
30
29
  return f"Invalid output '{output}'. Use: text, json"
31
- if not (plane or "").strip():
32
- valid_planes = diagnostic_plane_choices()
33
- if output == "json":
34
- return (
35
- "{"
36
- f"\"ok\": false, \"error\": \"Missing required argument: plane\", "
37
- "\"missing_argument\": \"plane\", "
38
- f"\"valid_planes\": {valid_planes!r}"
39
- "}"
40
- ).replace("'", '"')
41
- return (
42
- "Missing required argument: plane. "
43
- f"Use one of: {', '.join(valid_planes)}."
44
- )
45
30
 
46
31
  report = run_doctor(tier=tier, fix=fix, plane=plane)
47
32
  return format_report(report, fmt=output)