nexo-brain 7.9.4 → 7.9.5

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/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.4` is the current packaged-runtime line. Patch release that blocks the Brain 7.9.3 + Desktop 0.28.2 diary regression: canonical lifecycle plans now require real `session_diary` evidence (`wait_for_diary_write`) before `stop_session`, and canonical completion is rejected/retryable without that diary row. It also fixes npm CLI onboarding so `nexo-brain --version` and subcommands never launch the wizard when legacy/v2 calibration is already valid, commits setup calibration atomically only after the wizard completes, and adds `nexo-brain warmup-models` so install/update paths predownload the local mDeBERTa/BGE/reranker models. Verification: full Brain pytest (`2189 passed, 3 skipped, 1 xfailed, 5 xpassed`), release-readiness, npm pack dry-run, and coordinated Desktop v0.28.3 checks.
21
+ Version `7.9.5` is the current packaged-runtime line. Patch release that fixes canonical diary confirmation for Desktop: Brain now resolves the Desktop/Claude session UUID through NEXO SID aliases before checking `session_diary`, so archive/delete/app-exit can confirm diaries written by `nexo_session_diary_write` under the active `nexo-...` SID. Verification: `pytest tests/test_lifecycle_events.py` (28 passing) plus coordinated Desktop v0.28.4 shutdown/archive/delete checks.
22
+
23
+ Previously in `7.9.4`: patch release that blocks the Brain 7.9.3 + Desktop 0.28.2 diary regression: canonical lifecycle plans now require real `session_diary` evidence (`wait_for_diary_write`) before `stop_session`, and canonical completion is rejected/retryable without that diary row. It also fixes npm CLI onboarding so `nexo-brain --version` and subcommands never launch the wizard when legacy/v2 calibration is already valid, commits setup calibration atomically only after the wizard completes, and adds `nexo-brain warmup-models` so install/update paths predownload the local mDeBERTa/BGE/reranker models. Verification: full Brain pytest (`2189 passed, 3 skipped, 1 xfailed, 5 xpassed`), release-readiness, npm pack dry-run, and coordinated Desktop v0.28.3 checks.
22
24
 
23
25
  Previously in `7.9.3`: patch release that hardens Brain's canonical lifecycle plan for Desktop close/archive/delete/app-exit diary guarantees: `canonical_actions` now publish the v2 canonical shape (`type` plus `payload.prompt`) while keeping one-release compatibility mirrors (`kind` plus top-level `prompt`) for older Desktop clients. This lets Desktop execute resume → diary prompt → stop with one exact owner per lifecycle event and preserve Brain-side dedupe by event id. Targeted verification: `pytest tests/test_lifecycle_events.py` (25 passing) plus release-readiness after artifact sync.
24
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.9.4",
3
+ "version": "7.9.5",
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",
@@ -52,7 +52,7 @@ from __future__ import annotations
52
52
 
53
53
  import json
54
54
  import time
55
- from typing import Any, Dict, Optional
55
+ from typing import Any, Dict, List, Optional
56
56
 
57
57
  from db import get_db
58
58
  import lifecycle_prompts
@@ -84,13 +84,56 @@ def _normalise_payload(obj: Any) -> str:
84
84
  return "{}"
85
85
 
86
86
 
87
+ def _session_diary_session_ids(conn, session_id: str) -> List[str]:
88
+ """Return all session ids that can contain diary evidence for a lifecycle session.
89
+
90
+ Desktop passes Claude's conversation/session UUID as ``lifecycle_events.session_id``.
91
+ ``nexo_session_diary_write`` stores rows under the active NEXO SID (``nexo-...``).
92
+ The alias table links both values, so canonical diary confirmation must check the
93
+ direct id and its NEXO aliases.
94
+ """
95
+ raw = str(session_id or "").strip()
96
+ if not raw:
97
+ return []
98
+ ids: List[str] = [raw]
99
+ try:
100
+ rows = conn.execute(
101
+ "SELECT sid FROM session_claude_aliases "
102
+ "WHERE claude_session_id = ? ORDER BY last_seen DESC",
103
+ (raw,),
104
+ ).fetchall()
105
+ ids.extend(str(row[0]) for row in rows if row and row[0])
106
+ except Exception:
107
+ pass
108
+ try:
109
+ rows = conn.execute(
110
+ "SELECT sid FROM sessions "
111
+ "WHERE claude_session_id = ? OR external_session_id = ? "
112
+ "ORDER BY last_update_epoch DESC",
113
+ (raw, raw),
114
+ ).fetchall()
115
+ ids.extend(str(row[0]) for row in rows if row and row[0])
116
+ except Exception:
117
+ pass
118
+
119
+ deduped: List[str] = []
120
+ seen = set()
121
+ for sid in ids:
122
+ if sid and sid not in seen:
123
+ seen.add(sid)
124
+ deduped.append(sid)
125
+ return deduped
126
+
127
+
87
128
  def _max_session_diary_id(conn, session_id: str) -> int:
88
- if not session_id:
129
+ session_ids = _session_diary_session_ids(conn, session_id)
130
+ if not session_ids:
89
131
  return 0
132
+ placeholders = ",".join("?" for _ in session_ids)
90
133
  try:
91
134
  row = conn.execute(
92
- "SELECT COALESCE(MAX(id), 0) FROM session_diary WHERE session_id = ?",
93
- (str(session_id),),
135
+ f"SELECT COALESCE(MAX(id), 0) FROM session_diary WHERE session_id IN ({placeholders})",
136
+ tuple(session_ids),
94
137
  ).fetchone()
95
138
  except Exception:
96
139
  return 0
@@ -156,25 +199,30 @@ def _session_diary_evidence(
156
199
  if not session_id or not dispatched_at:
157
200
  return None
158
201
  checkpoint_id = _diary_checkpoint_from_actions_json(actions_json)
202
+ session_ids = _session_diary_session_ids(conn, session_id)
203
+ if not session_ids:
204
+ return None
205
+ placeholders = ",".join("?" for _ in session_ids)
159
206
  try:
160
207
  if checkpoint_id > 0:
161
208
  row = conn.execute(
162
- "SELECT id, created_at FROM session_diary "
163
- "WHERE session_id = ? AND id > ? ORDER BY id ASC LIMIT 1",
164
- (str(session_id), int(checkpoint_id)),
209
+ "SELECT id, created_at, session_id FROM session_diary "
210
+ f"WHERE session_id IN ({placeholders}) AND id > ? ORDER BY id ASC LIMIT 1",
211
+ (*session_ids, int(checkpoint_id)),
165
212
  ).fetchone()
166
213
  else:
167
214
  row = conn.execute(
168
- "SELECT id, created_at FROM session_diary "
169
- "WHERE session_id = ? AND created_at >= ? ORDER BY created_at ASC, id ASC LIMIT 1",
170
- (str(session_id), str(dispatched_at)),
215
+ "SELECT id, created_at, session_id FROM session_diary "
216
+ f"WHERE session_id IN ({placeholders}) AND created_at >= ? "
217
+ "ORDER BY created_at ASC, id ASC LIMIT 1",
218
+ (*session_ids, str(dispatched_at)),
171
219
  ).fetchone()
172
220
  except Exception:
173
221
  # Missing table on a minimal test harness — treat as "no diary".
174
222
  return None
175
223
  if row is None:
176
224
  return None
177
- return {"session_diary_id": row[0], "created_at": row[1]}
225
+ return {"session_diary_id": row[0], "created_at": row[1], "diary_session_id": row[2]}
178
226
 
179
227
 
180
228
  def _session_diary_since(conn, session_id: str, dispatched_at: Optional[str], actions_json: Optional[str] = None) -> bool: