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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/lifecycle_events.py +121 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.9.
|
|
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.
|
|
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.
|
|
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",
|
package/src/lifecycle_events.py
CHANGED
|
@@ -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)
|