nexo-brain 7.30.14 → 7.30.16

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.30.14",
3
+ "version": "7.30.16",
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,9 @@
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.30.14` is the current packaged-runtime line. Patch release over v7.30.13 - support tickets, provider capability discovery, task-close rearming, internal audit followups, and the memory-observation watchdog are aligned for Desktop-managed agents.
21
+ Version `7.30.16` is the current packaged-runtime line. Patch release over v7.30.14 - Desktop diagnostics can read embedding migration status without warming models, and the coordinated Desktop update path is covered for bundled model verification and obsolete managed model cleanup.
22
+
23
+ Previously in `7.30.14`: patch release over v7.30.13 - support tickets, provider capability discovery, task-close rearming, internal audit followups, and the memory-observation watchdog are aligned for Desktop-managed agents.
22
24
 
23
25
  Previously in `7.30.13`: patch release over v7.30.12 - Email NEXO monitor prompts now require a full IMAP body fetch before replying, and managed email defaults use the MXroute `Sent` folder instead of `INBOX.Sent`.
24
26
 
package/bin/nexo-brain.js CHANGED
@@ -666,6 +666,11 @@ function shouldSkipModelWarmup() {
666
666
  return ["1", "true", "yes", "on"].includes(flag);
667
667
  }
668
668
 
669
+ function shouldInstallLocalClassifierWarmupDeps() {
670
+ const flag = String(process.env.NEXO_LOCAL_CLASSIFIER || "").trim().toLowerCase();
671
+ return ["1", "true", "on", "auto"].includes(flag);
672
+ }
673
+
669
674
  function resolveSystemPython() {
670
675
  return run("which python3") || run("which python") || "python3";
671
676
  }
@@ -705,13 +710,15 @@ function installWarmupPythonDependencies(pythonPath, { quiet = false, installRun
705
710
  }
706
711
  }
707
712
 
708
- const classifierResult = spawnSync(
709
- pythonPath,
710
- [...pipCommon, ...WARMUP_PIP_PACKAGES],
711
- { stdio, timeout: WARMUP_TIMEOUT_MS }
712
- );
713
- if (classifierResult.status !== 0) {
714
- throw new Error("failed to install local classifier dependencies for model warmup");
713
+ if (shouldInstallLocalClassifierWarmupDeps()) {
714
+ const classifierResult = spawnSync(
715
+ pythonPath,
716
+ [...pipCommon, ...WARMUP_PIP_PACKAGES],
717
+ { stdio, timeout: WARMUP_TIMEOUT_MS }
718
+ );
719
+ if (classifierResult.status !== 0) {
720
+ throw new Error("failed to install local classifier dependencies for model warmup");
721
+ }
715
722
  }
716
723
  }
717
724
 
@@ -766,6 +773,157 @@ function runDesktopAwareModelWarmup(pythonPath, nexoHome = NEXO_HOME, options =
766
773
  runMandatoryModelWarmup(pythonPath, nexoHome, options);
767
774
  }
768
775
 
776
+ function slugifyLocalModelName(value) {
777
+ return String(value || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
778
+ }
779
+
780
+ function readManagedModelLock(dir) {
781
+ try {
782
+ const lockPath = path.join(dir, ".nexo-model-lock.json");
783
+ if (!fs.existsSync(lockPath)) return null;
784
+ const payload = JSON.parse(fs.readFileSync(lockPath, "utf8"));
785
+ return payload && typeof payload === "object" ? payload : null;
786
+ } catch (_) {
787
+ return null;
788
+ }
789
+ }
790
+
791
+ function isManagedModelRevisionDir(dir, { slug = "", revision = "" } = {}) {
792
+ const lock = readManagedModelLock(dir);
793
+ if (!lock) return false;
794
+ if (!lock.name || !lock.revision || (!lock.model_id && !lock.source_repo)) return false;
795
+ if (slug && slugifyLocalModelName(lock.name) !== slug) return false;
796
+ if (revision && String(lock.revision || "") !== String(revision || "")) return false;
797
+ return true;
798
+ }
799
+
800
+ function sha256File(filePath) {
801
+ return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
802
+ }
803
+
804
+ function cleanupObsoleteRuntimeLlmModels(runtimeModelsDir, manifest, { reason = "install" } = {}) {
805
+ if (String(process.env.NEXO_KEEP_OBSOLETE_LLM_MODELS || "").trim() === "1") {
806
+ log(` Keeping obsolete LLM models by NEXO_KEEP_OBSOLETE_LLM_MODELS=1 (${reason}).`);
807
+ return [];
808
+ }
809
+ if (!fs.existsSync(runtimeModelsDir)) return [];
810
+
811
+ const allowed = new Map();
812
+ for (const spec of manifest.models || []) {
813
+ const slug = slugifyLocalModelName(spec.name || "");
814
+ const revision = String(spec.revision || "").trim();
815
+ if (!slug || !revision) continue;
816
+ if (!allowed.has(slug)) allowed.set(slug, new Set());
817
+ allowed.get(slug).add(revision);
818
+ }
819
+
820
+ const removed = [];
821
+ for (const slugEntry of fs.readdirSync(runtimeModelsDir, { withFileTypes: true })) {
822
+ if (!slugEntry.isDirectory()) continue;
823
+ const slug = slugEntry.name;
824
+ if (slug.startsWith(".")) continue;
825
+ if (slug === "_hf-cache") continue;
826
+ const slugDir = path.join(runtimeModelsDir, slug);
827
+ const allowedRevisions = allowed.get(slug) || new Set();
828
+ for (const revisionEntry of fs.readdirSync(slugDir, { withFileTypes: true })) {
829
+ if (!revisionEntry.isDirectory()) continue;
830
+ const revision = revisionEntry.name;
831
+ const revisionDir = path.join(slugDir, revision);
832
+ if (allowedRevisions.has(revision)) continue;
833
+ if (!isManagedModelRevisionDir(revisionDir, { slug, revision })) continue;
834
+ fs.rmSync(revisionDir, { recursive: true, force: true });
835
+ removed.push(path.relative(runtimeModelsDir, revisionDir));
836
+ }
837
+ try {
838
+ if (fs.readdirSync(slugDir).length === 0) fs.rmdirSync(slugDir);
839
+ } catch (_) {}
840
+ }
841
+
842
+ if (removed.length > 0) {
843
+ log(` Removed ${removed.length} obsolete managed LLM model revision(s) (${reason}).`);
844
+ }
845
+ return removed;
846
+ }
847
+
848
+ function copyBundledLlmModelsToRuntime(nexoHome = NEXO_HOME, {
849
+ reason = "install",
850
+ bundledModelsDir = path.join(__dirname, "..", "models"),
851
+ manifestPath = path.join(__dirname, "..", "src", "local_model_manifest.json"),
852
+ } = {}) {
853
+ // OFFLINE-FIRST: copy bundled LLM models to runtime/models BEFORE warmup,
854
+ // so fastembed finds them locally and skips HuggingFace downloads.
855
+ // Bundle layout: resources/brain-bundle/models/<source-repo-name>/<all files>.
856
+ // Target layout: <NEXO_HOME>/runtime/models/<spec.name slugified>/<revision>/<files>.
857
+ // We map by source_repo basename to match local_model_manifest.json.
858
+ if (!fs.existsSync(bundledModelsDir)) return 0;
859
+ try {
860
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
861
+ const runtimeModelsDir = path.join(nexoHome, "runtime", "models");
862
+ let modelsCopied = 0;
863
+ for (const spec of manifest.models || []) {
864
+ // Bundle layout supports either model_id basename (e.g.
865
+ // "bge-base-en-v1.5" from "BAAI/bge-base-en-v1.5") or source_repo
866
+ // basename (e.g. "bge-base-en-v1.5-onnx-q" from "qdrant/...").
867
+ const modelIdName = (spec.model_id || "").split("/").pop();
868
+ const sourceRepoName = (spec.source_repo || "").split("/").pop();
869
+ let sourceDir = path.join(bundledModelsDir, sourceRepoName);
870
+ if (!fs.existsSync(sourceDir)) {
871
+ sourceDir = path.join(bundledModelsDir, modelIdName);
872
+ }
873
+ if (!fs.existsSync(sourceDir)) continue;
874
+ const slug = slugifyLocalModelName(spec.name || "");
875
+ const targetDir = path.join(runtimeModelsDir, slug, spec.revision);
876
+ fs.mkdirSync(targetDir, { recursive: true });
877
+ const missingFiles = [];
878
+ for (const f of (spec.required_files || [])) {
879
+ const src = path.join(sourceDir, f.path);
880
+ const dst = path.join(targetDir, f.path);
881
+ if (!fs.existsSync(src)) {
882
+ missingFiles.push(f.path);
883
+ continue;
884
+ }
885
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
886
+ let shouldCopy = !fs.existsSync(dst) || (f.size && fs.statSync(dst).size !== f.size);
887
+ if (!shouldCopy && f.sha256 && sha256File(dst) !== f.sha256) {
888
+ shouldCopy = true;
889
+ }
890
+ if (shouldCopy) {
891
+ fs.copyFileSync(src, dst);
892
+ }
893
+ if (f.size && fs.statSync(dst).size !== f.size) {
894
+ missingFiles.push(`${f.path}:size`);
895
+ continue;
896
+ }
897
+ if (f.sha256) {
898
+ const actual = sha256File(dst);
899
+ if (actual !== f.sha256) {
900
+ missingFiles.push(`${f.path}:sha256`);
901
+ }
902
+ }
903
+ }
904
+ if (missingFiles.length) {
905
+ log(` WARN: bundled LLM model ${spec.name} incomplete (${missingFiles.join(", ")})`);
906
+ continue;
907
+ }
908
+ // Write the lock file to match revision (avoids re-download).
909
+ fs.writeFileSync(path.join(targetDir, ".nexo-model-lock.json"), JSON.stringify({
910
+ name: spec.name, kind: spec.kind, model_id: spec.model_id,
911
+ source_repo: spec.source_repo, revision: spec.revision, model_file: spec.model_file,
912
+ required_files: spec.required_files,
913
+ }, null, 2));
914
+ modelsCopied++;
915
+ }
916
+ if (modelsCopied > 0) log(` Copied ${modelsCopied} pre-bundled LLM model(s) (offline, ${reason}).`);
917
+ if (modelsCopied > 0 && modelsCopied === (manifest.models || []).length) {
918
+ cleanupObsoleteRuntimeLlmModels(runtimeModelsDir, manifest, { reason });
919
+ }
920
+ return modelsCopied;
921
+ } catch (err) {
922
+ log(` WARN: bundled models copy failed during ${reason}: ${err.message}`);
923
+ return 0;
924
+ }
925
+ }
926
+
769
927
  async function runWarmupModelsCommand(args) {
770
928
  const dryRun = args.includes("--dry-run");
771
929
  const json = args.includes("--json");
@@ -3216,6 +3374,7 @@ async function runSetup() {
3216
3374
  log(" Python dependencies reconciled.");
3217
3375
  }
3218
3376
 
3377
+ copyBundledLlmModelsToRuntime(NEXO_HOME, { reason: "update" });
3219
3378
  const migPythonForWarmup = findVenvPython(NEXO_HOME) || "python3";
3220
3379
  runDesktopAwareModelWarmup(migPythonForWarmup, NEXO_HOME, { reason: "update", installRuntimeDeps: false });
3221
3380
 
@@ -3492,6 +3651,7 @@ async function runSetup() {
3492
3651
  stampRuntimeRepairBaseline(NEXO_HOME, "bin.nexo-brain.same-version-repair")
3493
3652
  );
3494
3653
 
3654
+ copyBundledLlmModelsToRuntime(NEXO_HOME, { reason: "repair" });
3495
3655
  runDesktopAwareModelWarmup(syncPython, NEXO_HOME, { reason: "repair" });
3496
3656
  logMacPermissionsNotice(NEXO_HOME, syncPython);
3497
3657
 
@@ -3935,71 +4095,7 @@ async function runSetup() {
3935
4095
  }
3936
4096
  log("Dependencies installed.");
3937
4097
 
3938
- // OFFLINE-FIRST: copy bundled LLM models to runtime/models BEFORE warmup,
3939
- // so fastembed finds them locally and skips the ~217MB HuggingFace download.
3940
- // Bundle layout: resources/brain-bundle/models/<source-repo-name>/<all files>.
3941
- // Target layout: <NEXO_HOME>/runtime/models/<spec.name slugified>/<revision>/<files>.
3942
- // We map by source_repo basename to match local_model_manifest.json.
3943
- const bundledModelsDir = path.join(__dirname, "..", "models");
3944
- if (fs.existsSync(bundledModelsDir)) {
3945
- try {
3946
- const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "src", "local_model_manifest.json"), "utf8"));
3947
- const runtimeModelsDir = path.join(NEXO_HOME, "runtime", "models");
3948
- let modelsCopied = 0;
3949
- for (const spec of manifest.models || []) {
3950
- // Bundle layout supports either model_id basename (e.g.
3951
- // "bge-base-en-v1.5" from "BAAI/bge-base-en-v1.5") or source_repo
3952
- // basename (e.g. "bge-base-en-v1.5-onnx-q" from "qdrant/...").
3953
- const modelIdName = (spec.model_id || "").split("/").pop();
3954
- const sourceRepoName = (spec.source_repo || "").split("/").pop();
3955
- let sourceDir = path.join(bundledModelsDir, modelIdName);
3956
- if (!fs.existsSync(sourceDir)) {
3957
- sourceDir = path.join(bundledModelsDir, sourceRepoName);
3958
- }
3959
- if (!fs.existsSync(sourceDir)) continue;
3960
- const slug = (spec.name || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3961
- const targetDir = path.join(runtimeModelsDir, slug, spec.revision);
3962
- fs.mkdirSync(targetDir, { recursive: true });
3963
- const missingFiles = [];
3964
- for (const f of (spec.required_files || [])) {
3965
- const src = path.join(sourceDir, f.path);
3966
- const dst = path.join(targetDir, f.path);
3967
- if (!fs.existsSync(src)) {
3968
- missingFiles.push(f.path);
3969
- continue;
3970
- }
3971
- fs.mkdirSync(path.dirname(dst), { recursive: true });
3972
- if (!fs.existsSync(dst) || (f.size && fs.statSync(dst).size !== f.size)) {
3973
- fs.copyFileSync(src, dst);
3974
- }
3975
- if (f.size && fs.statSync(dst).size !== f.size) {
3976
- missingFiles.push(`${f.path}:size`);
3977
- continue;
3978
- }
3979
- if (f.sha256) {
3980
- const actual = crypto.createHash("sha256").update(fs.readFileSync(dst)).digest("hex");
3981
- if (actual !== f.sha256) {
3982
- missingFiles.push(`${f.path}:sha256`);
3983
- }
3984
- }
3985
- }
3986
- if (missingFiles.length) {
3987
- log(` WARN: bundled LLM model ${spec.name} incomplete (${missingFiles.join(", ")})`);
3988
- continue;
3989
- }
3990
- // Write the lock file to match revision (avoids re-download).
3991
- fs.writeFileSync(path.join(targetDir, ".nexo-model-lock.json"), JSON.stringify({
3992
- name: spec.name, kind: spec.kind, model_id: spec.model_id,
3993
- source_repo: spec.source_repo, revision: spec.revision, model_file: spec.model_file,
3994
- required_files: spec.required_files,
3995
- }, null, 2));
3996
- modelsCopied++;
3997
- }
3998
- if (modelsCopied > 0) log(` Copied ${modelsCopied} pre-bundled LLM model(s) (offline).`);
3999
- } catch (err) {
4000
- log(` WARN: bundled models copy failed: ${err.message}`);
4001
- }
4002
- }
4098
+ copyBundledLlmModelsToRuntime(NEXO_HOME, { reason: "install" });
4003
4099
 
4004
4100
  runDesktopAwareModelWarmup(python, NEXO_HOME, { reason: "install", installRuntimeDeps: false });
4005
4101
 
@@ -5121,4 +5217,13 @@ if (isCliEntrypoint()) {
5121
5217
  console.error("Setup failed:", err.message);
5122
5218
  process.exit(1);
5123
5219
  });
5220
+ } else {
5221
+ module.exports = {
5222
+ cleanupObsoleteRuntimeLlmModels,
5223
+ copyBundledLlmModelsToRuntime,
5224
+ isManagedModelRevisionDir,
5225
+ readManagedModelLock,
5226
+ sha256File,
5227
+ slugifyLocalModelName,
5228
+ };
5124
5229
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.30.14",
3
+ "version": "7.30.16",
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",
@@ -1211,6 +1211,15 @@ def _local_classifier_install_command() -> list[str]:
1211
1211
  return cmd
1212
1212
 
1213
1213
 
1214
+ def _local_classifier_auto_install_enabled() -> bool:
1215
+ return str(os.environ.get("NEXO_LOCAL_CLASSIFIER", "")).strip().lower() in {
1216
+ "1",
1217
+ "true",
1218
+ "on",
1219
+ "auto",
1220
+ }
1221
+
1222
+
1214
1223
  def _install_local_classifier_worker() -> None:
1215
1224
  from classifier_local import MODEL_REVISION
1216
1225
 
@@ -1311,6 +1320,18 @@ def _maybe_install_local_classifier() -> None:
1311
1320
  "opt_out": True,
1312
1321
  })
1313
1322
  return
1323
+ if not _local_classifier_auto_install_enabled():
1324
+ from classifier_local import MODEL_REVISION
1325
+
1326
+ _write_classifier_install_log("[classifier-install] deferred: model is not bundled; set NEXO_LOCAL_CLASSIFIER=auto to install")
1327
+ _write_classifier_install_state({
1328
+ "installed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
1329
+ "model_revision": MODEL_REVISION,
1330
+ "deps_ok": False,
1331
+ "deferred": True,
1332
+ "reason": "not_bundled",
1333
+ })
1334
+ return
1314
1335
  try:
1315
1336
  deps_ok, versions, _missing = _probe_local_classifier_dependencies()
1316
1337
  if deps_ok:
@@ -17,6 +17,8 @@ from cognitive._core import (
17
17
  _get_db, _init_tables, _migrate_lifecycle, _migrate_co_activation,
18
18
  _migrate_memory_personalization,
19
19
  _auto_migrate_embeddings,
20
+ embedding_migration_status,
21
+ _active_embedding_context, _row_embedding_array, _embedding_migration_uses_shadow,
20
22
  _get_model, _get_reranker, rerank_results,
21
23
  embed, cosine_similarity, _array_to_blob, _blob_to_array,
22
24
  extract_temporal_date, redact_secrets,