nexo-brain 7.9.22 → 7.9.23

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.22",
3
+ "version": "7.9.23",
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.9.22` is the current packaged-runtime line. Patch release over `7.9.21`: Desktop lifecycle shutdowns now have an emergency Brain-side fallback diary path, so close/archive/app-exit can preserve title, goal, session ids, and transcript tail even when the live agent does not answer the injected diary prompt before shutdown.
21
+ Version `7.9.23` is the current packaged-runtime line. Patch release over `7.9.22`: Desktop lifecycle fallback diaries now enrich sparse lifecycle events from continuity snapshots, so app-exit fallback evidence preserves recent turn context even when the live agent does not answer the injected diary prompt before shutdown.
22
+
23
+ Previously in `7.9.22`: Desktop lifecycle shutdowns gained an emergency Brain-side fallback diary path, so close/archive/app-exit can preserve title, goal, session ids, and transcript tail even when the live agent does not answer the injected diary prompt before shutdown.
22
24
 
23
25
  Previously in `7.9.21`: LaunchAgent reload/repair now handles macOS already-loaded races by booting out jobs with modern launchctl forms, falling back to legacy load, and treating an already-loaded job as healthy only when it points at the expected plist.
24
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.9.22",
3
+ "version": "7.9.23",
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",
@@ -278,6 +278,18 @@ def _payload_lines(payload: Dict[str, Any]) -> List[str]:
278
278
  text = str(raw or "").strip()
279
279
  if text:
280
280
  lines.append(text)
281
+ if not lines:
282
+ messages = payload.get("messages")
283
+ if isinstance(messages, list):
284
+ for item in messages[-12:]:
285
+ if isinstance(item, dict):
286
+ role = str(item.get("role") or item.get("type") or item.get("sender") or "message").strip()
287
+ text = str(item.get("content") or item.get("text") or "").strip()
288
+ else:
289
+ role = "message"
290
+ text = str(item or "").strip()
291
+ if text:
292
+ lines.append(f"{role}: {text}")
281
293
  if not lines:
282
294
  user = str(payload.get("last_user_message") or payload.get("latest_user_text") or "").strip()
283
295
  assistant = str(payload.get("last_assistant_message") or payload.get("latest_assistant_text") or "").strip()
@@ -288,6 +300,114 @@ def _payload_lines(payload: Dict[str, Any]) -> List[str]:
288
300
  return lines[-12:]
289
301
 
290
302
 
303
+ def _parse_payload_json(raw: Any) -> Dict[str, Any]:
304
+ try:
305
+ parsed = json.loads(raw or "{}")
306
+ except Exception:
307
+ return {}
308
+ return parsed if isinstance(parsed, dict) else {}
309
+
310
+
311
+ def _payload_has_context(payload: Dict[str, Any]) -> bool:
312
+ if not isinstance(payload, dict):
313
+ return False
314
+ if _payload_lines(payload):
315
+ return True
316
+ for key in ("current_goal", "last_user_message", "latest_user_text", "last_assistant_message", "latest_assistant_text"):
317
+ if str(payload.get(key) or "").strip():
318
+ return True
319
+ return False
320
+
321
+
322
+ def _continuity_payloads_for_event(
323
+ conn,
324
+ conversation_id: str,
325
+ session_id: str,
326
+ limit: int = 24,
327
+ ) -> List[Dict[str, Any]]:
328
+ """Return recent continuity payloads for richer emergency diaries."""
329
+ conv = str(conversation_id or "").strip()
330
+ sid = str(session_id or "").strip()
331
+ if not conv and not sid:
332
+ return []
333
+
334
+ where_parts: List[str] = []
335
+ params: List[Any] = []
336
+ if conv:
337
+ where_parts.append("conversation_id = ?")
338
+ params.append(conv)
339
+ if sid:
340
+ where_parts.append("session_id = ?")
341
+ params.append(sid)
342
+ where = " OR ".join(where_parts)
343
+
344
+ try:
345
+ rows = conn.execute(
346
+ "SELECT event_type, payload_json FROM continuity_snapshots "
347
+ f"WHERE ({where}) "
348
+ "ORDER BY id DESC LIMIT ?",
349
+ (*params, int(limit)),
350
+ ).fetchall()
351
+ except Exception:
352
+ return []
353
+
354
+ payloads: List[Dict[str, Any]] = []
355
+ for row in reversed(rows):
356
+ event_type = str(row[0] or "").strip()
357
+ if event_type and event_type not in {"turn_end", "app_exit", "desktop_snapshot", "close", "archive"}:
358
+ continue
359
+ payload = _parse_payload_json(row[1] if len(row) > 1 else "")
360
+ if payload:
361
+ payloads.append(payload)
362
+ return payloads
363
+
364
+
365
+ def _dedupe_lines(lines: List[str]) -> List[str]:
366
+ seen = set()
367
+ result: List[str] = []
368
+ for raw in lines:
369
+ line = str(raw or "").strip()
370
+ if not line or line in seen:
371
+ continue
372
+ seen.add(line)
373
+ result.append(line)
374
+ return result
375
+
376
+
377
+ def _enrich_payload_from_continuity(
378
+ conn,
379
+ payload: Dict[str, Any],
380
+ conversation_id: str,
381
+ session_id: str,
382
+ ) -> Dict[str, Any]:
383
+ """Fill sparse lifecycle payloads from durable continuity snapshots."""
384
+ enriched = dict(payload or {})
385
+ continuity_payloads = _continuity_payloads_for_event(conn, conversation_id, session_id)
386
+ if not continuity_payloads:
387
+ return enriched
388
+
389
+ latest = continuity_payloads[-1]
390
+ for key in (
391
+ "title",
392
+ "current_goal",
393
+ "last_user_message",
394
+ "latest_user_text",
395
+ "last_assistant_message",
396
+ "latest_assistant_text",
397
+ ):
398
+ if not str(enriched.get(key) or "").strip() and str(latest.get(key) or "").strip():
399
+ enriched[key] = latest[key]
400
+
401
+ continuity_lines: List[str] = []
402
+ for item in continuity_payloads:
403
+ continuity_lines.extend(_payload_lines(item))
404
+ continuity_lines = _dedupe_lines(continuity_lines)
405
+ current_lines = _payload_lines(enriched)
406
+ if len(continuity_lines) > len(current_lines) or not _payload_has_context(enriched):
407
+ enriched["transcript_tail"] = continuity_lines[-12:]
408
+ return enriched
409
+
410
+
291
411
  def write_fallback_diary_for_lifecycle_event(
292
412
  event_id: str,
293
413
  reason: str = "",
@@ -341,6 +461,7 @@ def write_fallback_diary_for_lifecycle_event(
341
461
  payload = {}
342
462
  except Exception:
343
463
  payload = {}
464
+ payload = _enrich_payload_from_continuity(conn, payload, conversation_id, session_id)
344
465
 
345
466
  title = str(payload.get("title") or conversation_id or event_id).strip()
346
467
  transcript_lines = _payload_lines(payload)