nexo-brain 7.30.16 → 7.30.18
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 +5 -1
- package/package.json +1 -1
- package/src/auto_update.py +99 -0
- package/src/scripts/nexo-send-reply.py +182 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.18",
|
|
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,11 @@
|
|
|
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.
|
|
21
|
+
Version `7.30.18` is the current packaged-runtime line. Patch release over v7.30.17 - managed email replies now resolve the active assistant identity per account, replacing stale generated signatures without touching custom signatures.
|
|
22
|
+
|
|
23
|
+
Previously in `7.30.17`: patch release over v7.30.16 - F0.6 repairs promoted helper imports for personal scripts by adding a core-backed compatibility shim without duplicating the script catalog.
|
|
24
|
+
|
|
25
|
+
Previously in `7.30.16`: 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
26
|
|
|
23
27
|
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.
|
|
24
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.18",
|
|
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",
|
package/src/auto_update.py
CHANGED
|
@@ -3154,6 +3154,103 @@ def _ensure_f06_legacy_shims() -> None:
|
|
|
3154
3154
|
paths.finalize_backup_snapshot(conflict_root)
|
|
3155
3155
|
|
|
3156
3156
|
|
|
3157
|
+
def _ensure_f06_personal_script_import_shims() -> None:
|
|
3158
|
+
"""Expose promoted core helpers to legacy personal scripts.
|
|
3159
|
+
|
|
3160
|
+
Some operator-owned scripts are executed directly from
|
|
3161
|
+
``NEXO_HOME/personal/scripts`` and therefore only get that directory on
|
|
3162
|
+
``sys.path``. When Block E.6 promoted ``nexo_personal_automation.py`` into
|
|
3163
|
+
``core/scripts``, those direct scripts lost the bare import
|
|
3164
|
+
``from nexo_personal_automation import ...``. Keep the source of truth in
|
|
3165
|
+
core, but add a tiny compatibility entry in personal/scripts.
|
|
3166
|
+
"""
|
|
3167
|
+
|
|
3168
|
+
helper_names = ("nexo_personal_automation.py",)
|
|
3169
|
+
core_scripts = NEXO_HOME / "core" / "scripts"
|
|
3170
|
+
personal_scripts = NEXO_HOME / "personal" / "scripts"
|
|
3171
|
+
stub_marker = "# Auto-generated by NEXO F0.6 personal import shim."
|
|
3172
|
+
|
|
3173
|
+
def _same_file(a: Path, b: Path) -> bool:
|
|
3174
|
+
try:
|
|
3175
|
+
return a.is_file() and b.is_file() and a.read_bytes() == b.read_bytes()
|
|
3176
|
+
except Exception:
|
|
3177
|
+
return False
|
|
3178
|
+
|
|
3179
|
+
def _managed_stub(path: Path) -> bool:
|
|
3180
|
+
try:
|
|
3181
|
+
return path.is_file() and path.read_text(encoding="utf-8", errors="ignore").startswith(stub_marker)
|
|
3182
|
+
except Exception:
|
|
3183
|
+
return False
|
|
3184
|
+
|
|
3185
|
+
def _write_stub(shim: Path, target: Path) -> None:
|
|
3186
|
+
relative = os.path.relpath(str(target), str(shim.parent))
|
|
3187
|
+
shim.write_text(
|
|
3188
|
+
f"""{stub_marker}
|
|
3189
|
+
from __future__ import annotations
|
|
3190
|
+
|
|
3191
|
+
import importlib.util as _importlib_util
|
|
3192
|
+
from pathlib import Path as _Path
|
|
3193
|
+
|
|
3194
|
+
_TARGET = (_Path(__file__).resolve().parent / {relative!r}).resolve()
|
|
3195
|
+
_SPEC = _importlib_util.spec_from_file_location("_nexo_core_personal_automation", _TARGET)
|
|
3196
|
+
if _SPEC is None or _SPEC.loader is None:
|
|
3197
|
+
raise ImportError(f"Cannot load NEXO core helper at {{_TARGET}}")
|
|
3198
|
+
_MODULE = _importlib_util.module_from_spec(_SPEC)
|
|
3199
|
+
_SPEC.loader.exec_module(_MODULE)
|
|
3200
|
+
__all__ = getattr(_MODULE, "__all__", [name for name in vars(_MODULE) if not name.startswith("_")])
|
|
3201
|
+
globals().update({{name: getattr(_MODULE, name) for name in __all__}})
|
|
3202
|
+
""",
|
|
3203
|
+
encoding="utf-8",
|
|
3204
|
+
)
|
|
3205
|
+
|
|
3206
|
+
try:
|
|
3207
|
+
personal_scripts.mkdir(parents=True, exist_ok=True)
|
|
3208
|
+
except Exception as exc:
|
|
3209
|
+
_log(f"[F0.6 shim] could not create personal/scripts for import shims: {exc}")
|
|
3210
|
+
return
|
|
3211
|
+
|
|
3212
|
+
for name in helper_names:
|
|
3213
|
+
target = core_scripts / name
|
|
3214
|
+
if not target.is_file():
|
|
3215
|
+
continue
|
|
3216
|
+
shim = personal_scripts / name
|
|
3217
|
+
|
|
3218
|
+
if shim.is_symlink():
|
|
3219
|
+
try:
|
|
3220
|
+
if shim.resolve(strict=False) == target.resolve(strict=False):
|
|
3221
|
+
continue
|
|
3222
|
+
except Exception:
|
|
3223
|
+
pass
|
|
3224
|
+
try:
|
|
3225
|
+
shim.unlink()
|
|
3226
|
+
except Exception as exc:
|
|
3227
|
+
_log(f"[F0.6 shim] could not replace personal import symlink {name}: {exc}")
|
|
3228
|
+
continue
|
|
3229
|
+
|
|
3230
|
+
if shim.exists():
|
|
3231
|
+
if shim.is_file() and (_same_file(shim, target) or _managed_stub(shim)):
|
|
3232
|
+
try:
|
|
3233
|
+
shim.unlink()
|
|
3234
|
+
except Exception as exc:
|
|
3235
|
+
_log(f"[F0.6 shim] could not replace personal import copy {name}: {exc}")
|
|
3236
|
+
continue
|
|
3237
|
+
else:
|
|
3238
|
+
_log(f"[F0.6 shim] preserving distinct personal import helper {shim}")
|
|
3239
|
+
continue
|
|
3240
|
+
|
|
3241
|
+
try:
|
|
3242
|
+
relative = os.path.relpath(str(target), str(shim.parent))
|
|
3243
|
+
shim.symlink_to(relative)
|
|
3244
|
+
continue
|
|
3245
|
+
except Exception as exc:
|
|
3246
|
+
_log(f"[F0.6 shim] symlink create failed for personal import helper {name}: {exc}")
|
|
3247
|
+
|
|
3248
|
+
try:
|
|
3249
|
+
_write_stub(shim, target)
|
|
3250
|
+
except Exception as exc:
|
|
3251
|
+
_log(f"[F0.6 shim] stub create failed for personal import helper {name}: {exc}")
|
|
3252
|
+
|
|
3253
|
+
|
|
3157
3254
|
def _rewrite_f06_launch_agents() -> int:
|
|
3158
3255
|
"""Rewrite lingering LaunchAgent paths to canonical F0.6 locations."""
|
|
3159
3256
|
import re as _re
|
|
@@ -3279,6 +3376,7 @@ def _maybe_migrate_to_f06_layout() -> None:
|
|
|
3279
3376
|
_promote_packaged_runtime_code_to_core()
|
|
3280
3377
|
if _f06_live_legacy_paths():
|
|
3281
3378
|
_ensure_f06_legacy_shims()
|
|
3379
|
+
_ensure_f06_personal_script_import_shims()
|
|
3282
3380
|
_cleanup_f06_root_residue()
|
|
3283
3381
|
try:
|
|
3284
3382
|
_rewrite_f06_launch_agents()
|
|
@@ -3498,6 +3596,7 @@ def _maybe_migrate_to_f06_layout() -> None:
|
|
|
3498
3596
|
except Exception as e:
|
|
3499
3597
|
_log(f"[F0.6] marker write failed: {e}")
|
|
3500
3598
|
_ensure_f06_legacy_shims()
|
|
3599
|
+
_ensure_f06_personal_script_import_shims()
|
|
3501
3600
|
_cleanup_f06_root_residue()
|
|
3502
3601
|
try:
|
|
3503
3602
|
rewritten = _rewrite_f06_launch_agents()
|
|
@@ -96,6 +96,22 @@ RESOLUTION_PATTERNS = (
|
|
|
96
96
|
)
|
|
97
97
|
|
|
98
98
|
_REPLY_EVENT_CONFIDENCE = float(os.environ.get("NEXO_REPLY_EVENT_CONFIDENCE", "0.72"))
|
|
99
|
+
_GENERIC_AGENT_EMAIL_NAMES = {
|
|
100
|
+
"admin",
|
|
101
|
+
"agent",
|
|
102
|
+
"alerts",
|
|
103
|
+
"contact",
|
|
104
|
+
"hello",
|
|
105
|
+
"info",
|
|
106
|
+
"mail",
|
|
107
|
+
"nexo",
|
|
108
|
+
"nexoagent",
|
|
109
|
+
"no-reply",
|
|
110
|
+
"noreply",
|
|
111
|
+
"notifications",
|
|
112
|
+
"reply",
|
|
113
|
+
"support",
|
|
114
|
+
}
|
|
99
115
|
_REPLY_EVENT_LABELS = (
|
|
100
116
|
("The reply acknowledges receipt or says the work starts now", "ack"),
|
|
101
117
|
("The reply makes a future commitment or promises an update later", "commitment"),
|
|
@@ -134,23 +150,182 @@ def normalize_reply_text(text):
|
|
|
134
150
|
return re.sub(r"\s+", " ", (text or "").strip()).strip()
|
|
135
151
|
|
|
136
152
|
|
|
137
|
-
def
|
|
153
|
+
def _clean_identity_value(value) -> str:
|
|
154
|
+
text = str(value or "").strip()
|
|
155
|
+
if not text:
|
|
156
|
+
return ""
|
|
157
|
+
return re.sub(r"\s+", " ", text)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _nested_identity_value(mapping: dict | None, *paths: tuple[str, ...]) -> str:
|
|
161
|
+
if not isinstance(mapping, dict):
|
|
162
|
+
return ""
|
|
163
|
+
for path in paths:
|
|
164
|
+
current = mapping
|
|
165
|
+
for key in path:
|
|
166
|
+
if not isinstance(current, dict):
|
|
167
|
+
current = None
|
|
168
|
+
break
|
|
169
|
+
current = current.get(key)
|
|
170
|
+
value = _clean_identity_value(current)
|
|
171
|
+
if value:
|
|
172
|
+
return value
|
|
173
|
+
return ""
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _sender_email(config: dict | None) -> str:
|
|
177
|
+
if not isinstance(config, dict):
|
|
178
|
+
return ""
|
|
179
|
+
sender = _clean_identity_value(config.get("email"))
|
|
180
|
+
if sender:
|
|
181
|
+
return sender
|
|
182
|
+
agent_account = config.get("agent_account")
|
|
183
|
+
if isinstance(agent_account, dict):
|
|
184
|
+
return _clean_identity_value(agent_account.get("email"))
|
|
185
|
+
return ""
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _metadata_identity_value(config: dict | None) -> str:
|
|
189
|
+
if not isinstance(config, dict):
|
|
190
|
+
return ""
|
|
191
|
+
sources = [config]
|
|
192
|
+
agent_account = config.get("agent_account")
|
|
193
|
+
if isinstance(agent_account, dict):
|
|
194
|
+
sources.append(agent_account)
|
|
195
|
+
for source in sources:
|
|
196
|
+
metadata = source.get("metadata")
|
|
197
|
+
value = _nested_identity_value(
|
|
198
|
+
metadata if isinstance(metadata, dict) else None,
|
|
199
|
+
("assistant_name",),
|
|
200
|
+
("agent_name",),
|
|
201
|
+
("display_name",),
|
|
202
|
+
("identity", "assistant_name"),
|
|
203
|
+
("identity", "name"),
|
|
204
|
+
)
|
|
205
|
+
if value:
|
|
206
|
+
return value
|
|
207
|
+
return ""
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _calibration_assistant_name() -> str:
|
|
211
|
+
try:
|
|
212
|
+
from calibration_runtime import load_runtime_calibration
|
|
213
|
+
|
|
214
|
+
payload = load_runtime_calibration()
|
|
215
|
+
except Exception:
|
|
216
|
+
payload = {}
|
|
217
|
+
return _nested_identity_value(
|
|
218
|
+
payload if isinstance(payload, dict) else None,
|
|
219
|
+
("user", "assistant_name"),
|
|
220
|
+
("assistant_name",),
|
|
221
|
+
("identity", "assistant_name"),
|
|
222
|
+
("identity", "name"),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _profile_assistant_name() -> str:
|
|
227
|
+
try:
|
|
228
|
+
from paths import brain_dir
|
|
229
|
+
|
|
230
|
+
profile_path = brain_dir() / "profile.json"
|
|
231
|
+
payload = json.loads(profile_path.read_text(encoding="utf-8")) if profile_path.is_file() else {}
|
|
232
|
+
except Exception:
|
|
233
|
+
payload = {}
|
|
234
|
+
return _nested_identity_value(
|
|
235
|
+
payload if isinstance(payload, dict) else None,
|
|
236
|
+
("assistant_name",),
|
|
237
|
+
("agent_name",),
|
|
238
|
+
("identity", "assistant_name"),
|
|
239
|
+
("identity", "name"),
|
|
240
|
+
("profile", "assistant_name"),
|
|
241
|
+
("profile", "identity", "assistant_name"),
|
|
242
|
+
("profile", "identity", "name"),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _operator_profile_assistant_name(default: str) -> str:
|
|
138
247
|
try:
|
|
139
248
|
from automation_controls import get_operator_profile
|
|
140
249
|
|
|
141
250
|
profile = get_operator_profile()
|
|
142
251
|
except Exception:
|
|
143
252
|
profile = {}
|
|
144
|
-
value =
|
|
145
|
-
return value
|
|
253
|
+
value = _clean_identity_value((profile or {}).get("assistant_name"))
|
|
254
|
+
return "" if value == default else value
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _assistant_from_sender(config: dict | None) -> str:
|
|
258
|
+
sender = _sender_email(config)
|
|
259
|
+
if "@" not in sender:
|
|
260
|
+
return ""
|
|
261
|
+
local = sender.rsplit("@", 1)[0].strip().strip("<>")
|
|
262
|
+
local = local.split("+", 1)[0].strip()
|
|
263
|
+
if not local or local.lower() in _GENERIC_AGENT_EMAIL_NAMES:
|
|
264
|
+
return ""
|
|
265
|
+
words = [word for word in re.split(r"[^A-Za-z0-9]+", local) if word]
|
|
266
|
+
if not words:
|
|
267
|
+
return ""
|
|
268
|
+
candidate = " ".join(word[:1].upper() + word[1:] for word in words)
|
|
269
|
+
return _clean_identity_value(candidate)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _assistant_display_name(default: str = "Nova", config: dict | None = None) -> str:
|
|
273
|
+
candidates = [
|
|
274
|
+
os.environ.get("NEXO_ASSISTANT_NAME", ""),
|
|
275
|
+
_calibration_assistant_name(),
|
|
276
|
+
_profile_assistant_name(),
|
|
277
|
+
_metadata_identity_value(config),
|
|
278
|
+
_operator_profile_assistant_name(default),
|
|
279
|
+
_assistant_from_sender(config),
|
|
280
|
+
]
|
|
281
|
+
for candidate in candidates:
|
|
282
|
+
value = _clean_identity_value(candidate)
|
|
283
|
+
if value:
|
|
284
|
+
return value
|
|
285
|
+
return default
|
|
146
286
|
|
|
147
287
|
|
|
148
288
|
def _signature_label(config: dict) -> str:
|
|
149
|
-
assistant_name = _assistant_display_name()
|
|
150
|
-
sender =
|
|
289
|
+
assistant_name = _assistant_display_name(config=config)
|
|
290
|
+
sender = _sender_email(config)
|
|
151
291
|
return f"{assistant_name} — {sender}" if sender else assistant_name
|
|
152
292
|
|
|
153
293
|
|
|
294
|
+
def _metadata_signature_value(config: dict | None) -> str:
|
|
295
|
+
if not isinstance(config, dict):
|
|
296
|
+
return ""
|
|
297
|
+
sources = [config]
|
|
298
|
+
agent_account = config.get("agent_account")
|
|
299
|
+
if isinstance(agent_account, dict):
|
|
300
|
+
sources.append(agent_account)
|
|
301
|
+
for source in sources:
|
|
302
|
+
metadata = source.get("metadata")
|
|
303
|
+
signature = _nested_identity_value(metadata if isinstance(metadata, dict) else None, ("signature",))
|
|
304
|
+
if signature:
|
|
305
|
+
return signature
|
|
306
|
+
return ""
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _autogenerated_signature_owner(signature: str, sender: str) -> str:
|
|
310
|
+
signature = _clean_identity_value(signature)
|
|
311
|
+
sender = _clean_identity_value(sender)
|
|
312
|
+
if not signature or not sender:
|
|
313
|
+
return ""
|
|
314
|
+
pattern = rf"^(.+?)\s+(?:—|-)\s+{re.escape(sender)}$"
|
|
315
|
+
match = re.match(pattern, signature, flags=re.IGNORECASE)
|
|
316
|
+
return _clean_identity_value(match.group(1)) if match else ""
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _presentation_signature(config: dict) -> str:
|
|
320
|
+
fallback = _signature_label(config)
|
|
321
|
+
metadata_signature = _metadata_signature_value(config)
|
|
322
|
+
owner = _autogenerated_signature_owner(metadata_signature, _sender_email(config))
|
|
323
|
+
assistant_name = _assistant_display_name(config=config)
|
|
324
|
+
if owner and assistant_name and owner.casefold() != assistant_name.casefold():
|
|
325
|
+
return fallback
|
|
326
|
+
return signature_from_config(config, fallback=fallback)
|
|
327
|
+
|
|
328
|
+
|
|
154
329
|
def _message_id_domain(config: dict) -> str:
|
|
155
330
|
sender = str((config or {}).get("email") or "").strip()
|
|
156
331
|
if "@" in sender:
|
|
@@ -381,7 +556,7 @@ def build_html_quoted(quote_file, quote_from, quote_date):
|
|
|
381
556
|
|
|
382
557
|
def send_email(config, to, cc, subject, body_text, body_html, in_reply_to, references, attachments=None):
|
|
383
558
|
msg = MIMEMultipart("mixed")
|
|
384
|
-
msg["From"] = formataddr((_assistant_display_name(), config["email"]))
|
|
559
|
+
msg["From"] = formataddr((_assistant_display_name(config=config), config["email"]))
|
|
385
560
|
msg["To"] = to
|
|
386
561
|
if cc:
|
|
387
562
|
msg["Cc"] = cc
|
|
@@ -510,7 +685,7 @@ def main(argv=None):
|
|
|
510
685
|
subject=args.subject,
|
|
511
686
|
body_text=body_text,
|
|
512
687
|
body_html=html_fragment,
|
|
513
|
-
signature=
|
|
688
|
+
signature=_presentation_signature(config),
|
|
514
689
|
include_signature=True,
|
|
515
690
|
)
|
|
516
691
|
body_text = presentation.body_text
|