nexo-brain 7.30.17 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.30.17",
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,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.17` is the current packaged-runtime line. 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.
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.
22
24
 
23
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.
24
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.30.17",
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",
@@ -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 _assistant_display_name(default: str = "Nova") -> str:
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 = str((profile or {}).get("assistant_name") or "").strip()
145
- return value or default
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 = str((config or {}).get("email") or "").strip()
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=signature_from_config(config, fallback=_signature_label(config)),
688
+ signature=_presentation_signature(config),
514
689
  include_signature=True,
515
690
  )
516
691
  body_text = presentation.body_text