nexo-brain 7.9.4 → 7.9.6

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.4",
3
+ "version": "7.9.6",
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.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.6` is the current packaged-runtime line. Patch release that closes continuity + MCP restart stability for installed users: Brain now persists conversation-scoped continuity snapshots and resume bundles, activates packaged runtimes atomically under `~/.nexo/core/current`, writes a durable `mcp-restart-required` marker on real version changes, and self-drains old MCP processes until the client restarts against the installed runtime. Coordinated Desktop v0.28.7 consumes the same contract so existing `brain-only` and `brain+desktop` installs reroute to the new runtime instead of running mixed old/new MCP code.
22
+
23
+ Previously in `7.9.5`: patch release that fixes canonical diary confirmation for Desktop: Brain 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.6 shutdown/archive/delete/app-exit checks.
24
+
25
+ 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
26
 
23
27
  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
28
 
package/bin/nexo-brain.js CHANGED
@@ -3249,6 +3249,10 @@ async function runSetup() {
3249
3249
  ' printf \'%s\\n\' "${NEXO_CODE%/}"',
3250
3250
  ' return 0',
3251
3251
  ' fi',
3252
+ ' if [ -f "$NEXO_HOME/core/current/cli.py" ]; then',
3253
+ ' printf \'%s\\n\' "$NEXO_HOME/core/current"',
3254
+ ' return 0',
3255
+ ' fi',
3252
3256
  ' if [ -f "$NEXO_HOME/core/cli.py" ]; then',
3253
3257
  ' printf \'%s\\n\' "$NEXO_HOME/core"',
3254
3258
  ' return 0',
@@ -3302,6 +3306,11 @@ async function runSetup() {
3302
3306
  ' exit 1',
3303
3307
  'fi',
3304
3308
  'CLI_PY="$NEXO_CODE/cli.py"',
3309
+ 'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/core/current/cli.py" ]; then',
3310
+ ' NEXO_CODE="$NEXO_HOME/core/current"',
3311
+ ' export NEXO_CODE',
3312
+ ' CLI_PY="$NEXO_HOME/core/current/cli.py"',
3313
+ 'fi',
3305
3314
  'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/core/cli.py" ]; then',
3306
3315
  ' NEXO_CODE="$NEXO_HOME/core"',
3307
3316
  ' export NEXO_CODE',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.9.4",
3
+ "version": "7.9.6",
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",
@@ -537,6 +537,10 @@ def _runtime_cli_wrapper_text() -> str:
537
537
  ' printf \'%s\\n\' "${NEXO_CODE%/}"\n'
538
538
  ' return 0\n'
539
539
  ' fi\n'
540
+ ' if [ -f "$NEXO_HOME/core/current/cli.py" ]; then\n'
541
+ ' printf \'%s\\n\' "$NEXO_HOME/core/current"\n'
542
+ ' return 0\n'
543
+ ' fi\n'
540
544
  ' if [ -f "$NEXO_HOME/core/cli.py" ]; then\n'
541
545
  ' printf \'%s\\n\' "$NEXO_HOME/core"\n'
542
546
  ' return 0\n'
@@ -590,6 +594,11 @@ def _runtime_cli_wrapper_text() -> str:
590
594
  ' exit 1\n'
591
595
  'fi\n'
592
596
  'CLI_PY="$NEXO_CODE/cli.py"\n'
597
+ 'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/core/current/cli.py" ]; then\n'
598
+ ' NEXO_CODE="$NEXO_HOME/core/current"\n'
599
+ ' export NEXO_CODE\n'
600
+ ' CLI_PY="$NEXO_HOME/core/current/cli.py"\n'
601
+ 'fi\n'
593
602
  'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/core/cli.py" ]; then\n'
594
603
  ' NEXO_CODE="$NEXO_HOME/core"\n'
595
604
  ' export NEXO_CODE\n'
package/src/cli.py CHANGED
@@ -47,6 +47,7 @@ import time
47
47
  from pathlib import Path
48
48
 
49
49
  from runtime_home import export_resolved_nexo_home
50
+ from runtime_versioning import build_mcp_status, clear_restart_required_marker
50
51
 
51
52
  NEXO_HOME = export_resolved_nexo_home()
52
53
  NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent)))
@@ -120,6 +121,82 @@ def _fetch_latest_version(timeout_seconds: int = 2) -> str | None:
120
121
  return latest
121
122
 
122
123
 
124
+ def _print_json_or_text(payload: dict, *, as_json: bool = False) -> int:
125
+ if as_json:
126
+ print(json.dumps(payload, ensure_ascii=False))
127
+ else:
128
+ print(json.dumps(payload, indent=2, ensure_ascii=False))
129
+ return 0 if payload.get("ok", True) else 1
130
+
131
+
132
+ def _mcp_status(args) -> int:
133
+ return _print_json_or_text(
134
+ build_mcp_status(client=getattr(args, "client", "") or ""),
135
+ as_json=bool(getattr(args, "json", False)),
136
+ )
137
+
138
+
139
+ def _mcp_clear_restart(args) -> int:
140
+ return _print_json_or_text(
141
+ clear_restart_required_marker(
142
+ client=getattr(args, "client", "") or "",
143
+ installed_version=getattr(args, "installed_version", "") or "",
144
+ process_version=getattr(args, "process_version", "") or "",
145
+ ),
146
+ as_json=bool(getattr(args, "json", False)),
147
+ )
148
+
149
+
150
+ def _continuity_snapshot_write(args) -> int:
151
+ from continuity import write_snapshot
152
+
153
+ result = write_snapshot(
154
+ conversation_id=args.conversation_id,
155
+ session_id=args.session_id or "",
156
+ external_session_id=args.external_session_id or "",
157
+ client=args.client or "",
158
+ event_type=args.event_type or "turn_end",
159
+ payload=args.payload or "",
160
+ trace_id=args.trace_id or "",
161
+ idempotency_key=args.idempotency_key or "",
162
+ )
163
+ return _print_json_or_text(result, as_json=bool(getattr(args, "json", False)))
164
+
165
+
166
+ def _continuity_snapshot_read(args) -> int:
167
+ from continuity import read_snapshot
168
+
169
+ result = read_snapshot(
170
+ conversation_id=args.conversation_id or "",
171
+ session_id=args.session_id or "",
172
+ limit=args.limit or 20,
173
+ )
174
+ return _print_json_or_text(result, as_json=bool(getattr(args, "json", False)))
175
+
176
+
177
+ def _continuity_resume_bundle(args) -> int:
178
+ from continuity import build_resume_bundle
179
+
180
+ result = build_resume_bundle(
181
+ conversation_id=args.conversation_id or "",
182
+ session_id=args.session_id or "",
183
+ external_session_id=args.external_session_id or "",
184
+ client=args.client or "",
185
+ token_budget=args.token_budget or 2000,
186
+ )
187
+ return _print_json_or_text(result, as_json=bool(getattr(args, "json", False)))
188
+
189
+
190
+ def _continuity_audit(args) -> int:
191
+ from continuity import continuity_audit
192
+
193
+ result = continuity_audit(
194
+ conversation_id=args.conversation_id,
195
+ limit=args.limit or 50,
196
+ )
197
+ return _print_json_or_text(result, as_json=bool(getattr(args, "json", False)))
198
+
199
+
123
200
  def _should_refresh_latest_version() -> bool:
124
201
  """Decide whether to hit the npm registry to refresh `latest` version.
125
202
 
@@ -3075,6 +3152,49 @@ def main():
3075
3152
  dashboard_parser = sub.add_parser("dashboard", help="Web dashboard control")
3076
3153
  dashboard_parser.add_argument("action", choices=["on", "off", "status"], help="Start, stop, or check dashboard")
3077
3154
 
3155
+ mcp_parser = sub.add_parser("mcp", help="MCP runtime status and restart state")
3156
+ mcp_sub = mcp_parser.add_subparsers(dest="mcp_command")
3157
+ mcp_status_p = mcp_sub.add_parser("status", help="Read the current runtime/MCP alignment state")
3158
+ mcp_status_p.add_argument("--client", default="", help="Optional client label such as claude_desktop or codex")
3159
+ mcp_status_p.add_argument("--json", action="store_true", help="JSON output")
3160
+ mcp_clear_p = mcp_sub.add_parser("clear-restart", help="Acknowledge that a client/session reloaded the new runtime")
3161
+ mcp_clear_p.add_argument("--client", default="", help="Client label such as claude_desktop or codex")
3162
+ mcp_clear_p.add_argument("--installed-version", default="")
3163
+ mcp_clear_p.add_argument("--process-version", default="")
3164
+ mcp_clear_p.add_argument("--json", action="store_true", help="JSON output")
3165
+
3166
+ continuity_parser = sub.add_parser("continuity", help="Continuity snapshots and resume bundles")
3167
+ continuity_sub = continuity_parser.add_subparsers(dest="continuity_command")
3168
+ cwrite_p = continuity_sub.add_parser("snapshot-write", help="Write a continuity snapshot")
3169
+ cwrite_p.add_argument("--conversation-id", required=True)
3170
+ cwrite_p.add_argument("--session-id", default="")
3171
+ cwrite_p.add_argument("--external-session-id", default="")
3172
+ cwrite_p.add_argument("--client", default="")
3173
+ cwrite_p.add_argument("--event-type", default="turn_end")
3174
+ cwrite_p.add_argument("--payload", default="")
3175
+ cwrite_p.add_argument("--trace-id", default="")
3176
+ cwrite_p.add_argument("--idempotency-key", default="")
3177
+ cwrite_p.add_argument("--json", action="store_true", help="JSON output")
3178
+
3179
+ cread_p = continuity_sub.add_parser("snapshot-read", help="Read continuity snapshots")
3180
+ cread_p.add_argument("--conversation-id", default="")
3181
+ cread_p.add_argument("--session-id", default="")
3182
+ cread_p.add_argument("--limit", type=int, default=20)
3183
+ cread_p.add_argument("--json", action="store_true", help="JSON output")
3184
+
3185
+ cbundle_p = continuity_sub.add_parser("resume-bundle", help="Build a continuity resume bundle")
3186
+ cbundle_p.add_argument("--conversation-id", default="")
3187
+ cbundle_p.add_argument("--session-id", default="")
3188
+ cbundle_p.add_argument("--external-session-id", default="")
3189
+ cbundle_p.add_argument("--client", default="")
3190
+ cbundle_p.add_argument("--token-budget", type=int, default=2000)
3191
+ cbundle_p.add_argument("--json", action="store_true", help="JSON output")
3192
+
3193
+ caudit_p = continuity_sub.add_parser("audit", help="Read the continuity audit timeline")
3194
+ caudit_p.add_argument("--conversation-id", required=True)
3195
+ caudit_p.add_argument("--limit", type=int, default=50)
3196
+ caudit_p.add_argument("--json", action="store_true", help="JSON output")
3197
+
3078
3198
  # -- desktop bridge (read-only, for NEXO Desktop and any external UI) --
3079
3199
  # v7.4.0 — lifecycle event bridge (guardian-claude-desktop-plan).
3080
3200
  lifecycle_parser = sub.add_parser("lifecycle", help="Conversation lifecycle event handler (v7.4 Desktop bridge)")
@@ -3268,6 +3388,24 @@ def main():
3268
3388
  return _clients_sync(args)
3269
3389
  clients_parser.print_help()
3270
3390
  return 0
3391
+ elif args.command == "mcp":
3392
+ if args.mcp_command == "status":
3393
+ return _mcp_status(args)
3394
+ if args.mcp_command == "clear-restart":
3395
+ return _mcp_clear_restart(args)
3396
+ mcp_parser.print_help()
3397
+ return 0
3398
+ elif args.command == "continuity":
3399
+ if args.continuity_command == "snapshot-write":
3400
+ return _continuity_snapshot_write(args)
3401
+ elif args.continuity_command == "snapshot-read":
3402
+ return _continuity_snapshot_read(args)
3403
+ elif args.continuity_command == "resume-bundle":
3404
+ return _continuity_resume_bundle(args)
3405
+ elif args.continuity_command == "audit":
3406
+ return _continuity_audit(args)
3407
+ continuity_parser.print_help()
3408
+ return 0
3271
3409
  elif args.command == "preferences":
3272
3410
  return _preferences(args)
3273
3411
  elif args.command == "doctor":