nexo-brain 6.0.1 → 6.0.2

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": "6.0.1",
3
+ "version": "6.0.2",
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 `6.0.1` is the current packaged-runtime line: hotfix on top of the 6.0.0 release. `protocol_settings.py` now treats the process as interactive when **either** stdin+stdout are TTYs **or** `NEXO_INTERACTIVE=1` is exported closes the gap where NEXO Desktop 0.12.0 spawned `claude` through pipes and Brain fell back to `lenient` even with a human in the loop. The `PostToolUse` hook also gains an inbox autodetect stage: when the session has unread `nexo_send` messages and has gone 60s+ without a heartbeat, it emits a `systemMessage` asking the agent to run `nexo_heartbeat` and consume them. Rate-limited to one reminder per minute per SID (new `hook_inbox_reminders` table, migration m42). Added `sessions.last_heartbeat_ts`, stamped by every successful heartbeat. `NEXO_INTERACTIVE` is an internal Brain↔Electron contract — not user-facing, not a resurrection of the removed `NEXO_PROTOCOL_STRICTNESS`.
21
+ Version `6.0.2` is the current packaged-runtime line: adds the reserved caller prefix `personal/*` so scripts living in `~/.nexo/scripts/` can invoke the automation backend with their own caller id without editing `src/resonance_map.py`. New kwarg `tier` (`"maximo"` / `"alto"` / `"medio"` / `"bajo"`) on `run_automation_prompt`, `run_automation_interactive`, `nexo_helper.run_automation_text`, `nexo_helper.run_automation_json`, and `nexo-agent-run.py --tier`. Precedence for `personal/*` callers: explicit `tier=` explicit `reasoning_effort=` `calibration.preferences.default_resonance` `DEFAULT_RESONANCE` (`alto`). Registered callers keep their behaviour unchanged. New guide: [`docs/personal-scripts-guide.md`](docs/personal-scripts-guide.md).
22
+
23
+ Previously in `6.0.1`: hotfix on top of the 6.0.0 release. `protocol_settings.py` now treats the process as interactive when **either** stdin+stdout are TTYs **or** `NEXO_INTERACTIVE=1` is exported — closes the gap where NEXO Desktop 0.12.0 spawned `claude` through pipes and Brain fell back to `lenient` even with a human in the loop. The `PostToolUse` hook also gains an inbox autodetect stage: when the session has unread `nexo_send` messages and has gone 60s+ without a heartbeat, it emits a `systemMessage` asking the agent to run `nexo_heartbeat` and consume them. Rate-limited to one reminder per minute per SID (new `hook_inbox_reminders` table, migration m42). Added `sessions.last_heartbeat_ts`, stamped by every successful heartbeat. `NEXO_INTERACTIVE` is an internal Brain↔Electron contract — not user-facing, not a resurrection of the removed `NEXO_PROTOCOL_STRICTNESS`.
22
24
 
23
25
  Previously in `6.0.0`: **BREAKING** tier-only setup. Onboarding asks for one resonance tier (`maximo`/`alto`/`medio`/`bajo`) and that choice drives every backend via `src/resonance_tiers.json`; the per-backend model/effort prompts are gone and the legacy `client_runtime_profiles.{claude_code,codex}.{model,reasoning_effort}` are silently purged from `schedule.json` on upgrade. Protocol strictness is no longer configurable — interactive TTY sessions run `strict`, non-TTY (crons, pipes, tests) run `lenient`; `NEXO_PROTOCOL_STRICTNESS` env, `preferences.protocol_strictness`, and the `default/normal/off/warn/soft` aliases are all removed. `preferences.show_pending_at_start` moves to NEXO Desktop's electron-store. The seven core hooks are now unified behind `src/hooks/manifest.json` (plugin and npm modes read the same file), two new hooks ship (`Notification` for live-session activity and `SubagentStop` for auto-closing stale `protocol_tasks`), and `auto_capture.py` is wired to both `UserPromptSubmit` and `PostToolUse` with a persistent 1h dedup table plus an automatic `nexo_learning_add` on correction matches. `~/.nexo/hooks_status.json` is published after every `registerAllCoreHooks()` so NEXO Desktop ≥0.12.0 can render Hooks activos X/Y. New `nexo-brain --skip` flag aliases `--yes`/`--defaults`. Full suite 1057 passed, 1 skipped.
24
26
 
@@ -1145,6 +1147,7 @@ If NEXO Brain is useful to you, consider:
1145
1147
  - **Share your experience** — tell others how you're using cognitive memory in your AI workflows
1146
1148
  - **Contribute** — see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Issues and PRs welcome
1147
1149
  - **Client parity / shared-brain maintenance** — see [docs/client-parity-checklist.md](docs/client-parity-checklist.md)
1150
+ - **Writing a personal script that calls the automation backend** — see [docs/personal-scripts-guide.md](docs/personal-scripts-guide.md)
1148
1151
 
1149
1152
  [![Star History Chart](https://api.star-history.com/svg?repos=wazionapps/nexo&type=Date)](https://star-history.com/#wazionapps/nexo&Date)
1150
1153
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "6.0.1",
3
+ "version": "6.0.2",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO Brain \u2014 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",
@@ -531,6 +531,7 @@ def run_automation_interactive(
531
531
  env: dict | None = None,
532
532
  preferences: dict | None = None,
533
533
  session_type: str = "interactive_chat",
534
+ tier: str = "",
534
535
  ) -> subprocess.CompletedProcess:
535
536
  """Launch an interactive Claude/Codex session with automation_runs logging.
536
537
 
@@ -564,8 +565,13 @@ def run_automation_interactive(
564
565
  user_default = ""
565
566
  if isinstance(prefs, dict):
566
567
  user_default = str(prefs.get("default_resonance") or "").strip()
568
+ # v6.0.2 — respect explicit ``tier`` override so personal/* callers
569
+ # can force a resonance without registering in the core map.
570
+ explicit_tier_arg = (tier or "").strip() or None
567
571
  resonance_tier = resolve_tier_for_caller(
568
- caller, user_default=user_default or None
572
+ caller,
573
+ user_default=user_default or None,
574
+ explicit_tier=explicit_tier_arg,
569
575
  )
570
576
  except Exception:
571
577
  resonance_tier = ""
@@ -848,6 +854,7 @@ def run_automation_prompt(
848
854
  env: dict | None = None,
849
855
  model: str = "",
850
856
  reasoning_effort: str = "",
857
+ tier: str = "",
851
858
  timeout: int = 300,
852
859
  output_format: str = "",
853
860
  append_system_prompt: str = "",
@@ -891,13 +898,22 @@ def run_automation_prompt(
891
898
  user_default = ""
892
899
  if isinstance(prefs, dict):
893
900
  user_default = str(prefs.get("default_resonance") or "").strip()
901
+ # v6.0.2 — ``tier`` kwarg propagates to the resolver as ``explicit_tier``
902
+ # so personal/* callers can pin their reasoning budget per-invocation
903
+ # without editing the registry. Registered callers remain unchanged.
904
+ explicit_tier_arg = (tier or "").strip() or None
894
905
  # This raises UnregisteredCallerError if caller is unknown — the
895
906
  # same fail-closed rule we wanted. No silent fallback.
896
907
  resonance_tier = resolve_tier_for_caller(
897
- caller, user_default=user_default or None
908
+ caller,
909
+ user_default=user_default or None,
910
+ explicit_tier=explicit_tier_arg,
898
911
  )
899
912
  mapped_model, mapped_effort = resolve_model_and_effort(
900
- caller, selected_backend, user_default=user_default or None
913
+ caller,
914
+ selected_backend,
915
+ user_default=user_default or None,
916
+ explicit_tier=explicit_tier_arg,
901
917
  )
902
918
  if mapped_model and not model:
903
919
  model = mapped_model
@@ -213,6 +213,21 @@ ALL_REGISTERED_CALLERS: frozenset[str] = frozenset(
213
213
  )
214
214
 
215
215
 
216
+ # v6.0.2 — Reserved caller prefix for user-owned personal scripts that live
217
+ # outside this repo (``~/.nexo/scripts/``). Callers matching this prefix
218
+ # bypass the registry entirely: they cannot be required to register because
219
+ # they ship with each operator's own install. Instead, the script passes
220
+ # either an explicit ``tier`` (semantic) or a ``reasoning_effort`` (direct
221
+ # override) — or falls back to the user's ``default_resonance`` preference,
222
+ # and finally to ``DEFAULT_RESONANCE`` as the last line of defence.
223
+ #
224
+ # The prefix is NOT a loophole for new core scripts. Anything inside the
225
+ # ``src/`` tree or shipped via the core manifest continues to require a
226
+ # registered entry. The docs (``docs/personal-scripts-guide.md``) explain
227
+ # the split to any NEXO session helping an operator author a new script.
228
+ PERSONAL_CALLER_PREFIX = "personal/"
229
+
230
+
216
231
  class UnregisteredCallerError(ValueError):
217
232
  """Raised when a caller string is not in the resonance registry.
218
233
 
@@ -271,22 +286,55 @@ def _load_user_default_resonance() -> str:
271
286
  return ""
272
287
 
273
288
 
274
- def resolve_tier_for_caller(caller: str, user_default: str | None = None) -> str:
275
- """Return the resonance tier that should apply to ``caller``.
289
+ def _normalise_tier(candidate: str | None) -> str:
290
+ """Coerce a tier string to canonical lowercase; empty when invalid."""
291
+ if not candidate:
292
+ return ""
293
+ value = str(candidate).strip().lower()
294
+ return value if value in TIERS else ""
276
295
 
277
- - User-facing callers resolve to ``user_default`` (or ``DEFAULT_RESONANCE``
278
- if the user has no preference recorded).
279
- - System-owned callers resolve to their fixed tier.
280
- - Unknown callers raise ``UnregisteredCallerError``.
281
296
 
282
- When ``user_default`` is not passed, the function looks it up from the
283
- calibration.json preferences first and schedule.json second.
297
+ def resolve_tier_for_caller(
298
+ caller: str,
299
+ user_default: str | None = None,
300
+ *,
301
+ explicit_tier: str | None = None,
302
+ ) -> str:
303
+ """Return the resonance tier that should apply to ``caller``.
304
+
305
+ Resolution order:
306
+
307
+ 1. ``caller`` is empty → raise ``UnregisteredCallerError`` (same as v6.0.0).
308
+ 2. ``caller`` starts with ``PERSONAL_CALLER_PREFIX``:
309
+ a. ``explicit_tier`` if valid — semantic override from the script.
310
+ b. ``user_default`` if valid — operator's configured default.
311
+ c. Stored ``preferences.default_resonance`` via the loader.
312
+ d. ``DEFAULT_RESONANCE`` as the final fallback.
313
+ The registry is NEVER consulted for personal callers: scripts outside
314
+ the repo cannot register, and forcing them to pin a tier there would
315
+ defeat the whole purpose of the ``personal/`` contract.
316
+ 3. User-facing callers: user default → DEFAULT (unchanged).
317
+ 4. System-owned callers: fixed tier (unchanged).
318
+ 5. Anything else: ``UnregisteredCallerError`` (unchanged).
284
319
  """
285
320
  if not caller:
286
321
  raise UnregisteredCallerError(
287
322
  "caller= is required. Every automation subprocess must be registered "
288
323
  "in src/resonance_map.py so its reasoning budget is deliberate."
289
324
  )
325
+
326
+ if caller.startswith(PERSONAL_CALLER_PREFIX):
327
+ explicit = _normalise_tier(explicit_tier)
328
+ if explicit:
329
+ return explicit
330
+ from_user = _normalise_tier(user_default)
331
+ if from_user:
332
+ return from_user
333
+ from_prefs = _normalise_tier(_load_user_default_resonance())
334
+ if from_prefs:
335
+ return from_prefs
336
+ return DEFAULT_RESONANCE
337
+
290
338
  if caller in USER_FACING_CALLERS:
291
339
  resolved_default = user_default
292
340
  if resolved_default is None:
@@ -308,6 +356,8 @@ def resolve_model_and_effort(
308
356
  caller: str,
309
357
  backend: str,
310
358
  user_default: str | None = None,
359
+ *,
360
+ explicit_tier: str | None = None,
311
361
  ) -> Tuple[str, str]:
312
362
  """Return ``(model, reasoning_effort)`` for ``caller`` on ``backend``.
313
363
 
@@ -316,7 +366,9 @@ def resolve_model_and_effort(
316
366
  empty pair; the caller is expected to handle that by raising or by
317
367
  passing its own explicit model/effort arguments.
318
368
  """
319
- tier = resolve_tier_for_caller(caller, user_default=user_default)
369
+ tier = resolve_tier_for_caller(
370
+ caller, user_default=user_default, explicit_tier=explicit_tier
371
+ )
320
372
  backend_entry = _RESONANCE_TABLE.get(tier, {}).get(backend)
321
373
  if backend_entry is None:
322
374
  return "", ""
@@ -33,6 +33,20 @@ def main(argv: list[str] | None = None) -> int:
33
33
  parser.add_argument("--task-profile", default="", help="Automation task profile: default|fast|balanced|deep")
34
34
  parser.add_argument("--model", default="", help="Backend model hint")
35
35
  parser.add_argument("--reasoning-effort", default="", help="Backend reasoning effort/profile")
36
+ parser.add_argument(
37
+ "--tier",
38
+ default="",
39
+ help="Resonance tier — 'maximo'/'alto'/'medio'/'bajo'. "
40
+ "v6.0.2+ — used by personal/* callers to override the "
41
+ "resonance without editing src/resonance_map.py.",
42
+ )
43
+ parser.add_argument(
44
+ "--caller",
45
+ default="",
46
+ help="Registered caller id (e.g. nexo_chat) or 'personal/<id>' for "
47
+ "user-owned scripts. Required in practice; empty falls back "
48
+ "to 'agent_run/generic' for backward compatibility.",
49
+ )
36
50
  parser.add_argument("--timeout", type=int, default=AUTOMATION_SUBPROCESS_TIMEOUT, help="Timeout in seconds")
37
51
  parser.add_argument("--output-format", default="text", help="Requested output format")
38
52
  parser.add_argument("--allowed-tools", default="", help="Claude-style allowed tools contract")
@@ -52,11 +66,12 @@ def main(argv: list[str] | None = None) -> int:
52
66
  try:
53
67
  result = run_automation_prompt(
54
68
  prompt,
55
- caller=getattr(args, "caller", "") or "agent_run/generic",
69
+ caller=args.caller or "agent_run/generic",
56
70
  cwd=args.cwd or None,
57
71
  task_profile=args.task_profile,
58
72
  model=args.model,
59
73
  reasoning_effort=args.reasoning_effort,
74
+ tier=args.tier,
60
75
  timeout=args.timeout,
61
76
  output_format=args.output_format,
62
77
  append_system_prompt=append_system_prompt,
@@ -206,18 +206,29 @@ def run_automation_text(
206
206
  allowed_tools: str = DEFAULT_ALLOWED_TOOLS,
207
207
  append_system_prompt: str = "",
208
208
  include_bootstrap: bool = True,
209
+ caller: str = "",
210
+ tier: str = "",
209
211
  ) -> str:
210
212
  """Run the configured NEXO automation backend and return text output.
211
213
 
212
214
  This avoids hardcoding provider CLIs such as `claude -p` inside personal
213
215
  scripts. The runtime routes the call through the selected backend and its
214
216
  configured model profile.
217
+
218
+ Personal scripts (those living in ``~/.nexo/scripts/``) should pass
219
+ ``caller="personal/<descriptive-id>"`` and optionally ``tier="alto"``
220
+ (or another canonical tier) to pick their resonance without editing the
221
+ NEXO Brain repo. See ``docs/personal-scripts-guide.md`` for the rules.
215
222
  """
216
223
  runner = NEXO_HOME / "scripts" / "nexo-agent-run.py"
217
224
  if not runner.exists():
218
225
  raise RuntimeError(f"Automation runner not found: {runner}")
219
226
 
220
227
  cmd = [sys.executable, str(runner), "--prompt", prompt, "--output-format", "text"]
228
+ if caller:
229
+ cmd.extend(["--caller", caller])
230
+ if tier:
231
+ cmd.extend(["--tier", tier])
221
232
  if model:
222
233
  cmd.extend(["--model", model])
223
234
  if reasoning_effort:
@@ -261,13 +272,25 @@ def run_automation_json(
261
272
  allowed_tools: str = DEFAULT_ALLOWED_TOOLS,
262
273
  append_system_prompt: str = "",
263
274
  include_bootstrap: bool = True,
275
+ caller: str = "",
276
+ tier: str = "",
264
277
  ) -> dict:
265
- """Run the configured backend and return a parsed JSON object."""
278
+ """Run the configured backend and return a parsed JSON object.
279
+
280
+ v6.0.2 adds ``caller`` and ``tier`` kwargs so personal scripts
281
+ (``~/.nexo/scripts/``) can identify themselves and pick a resonance
282
+ without registering in the core repo. See
283
+ ``docs/personal-scripts-guide.md``.
284
+ """
266
285
  runner = NEXO_HOME / "scripts" / "nexo-agent-run.py"
267
286
  if not runner.exists():
268
287
  raise RuntimeError(f"Automation runner not found: {runner}")
269
288
 
270
289
  cmd = [sys.executable, str(runner), "--prompt", prompt, "--output-format", "json"]
290
+ if caller:
291
+ cmd.extend(["--caller", caller])
292
+ if tier:
293
+ cmd.extend(["--tier", tier])
271
294
  if model:
272
295
  cmd.extend(["--model", model])
273
296
  if reasoning_effort: