nexo-brain 5.9.1 → 5.10.0

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": "5.9.1",
3
+ "version": "5.10.0",
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 `5.9.1` is the current packaged-runtime line: adds `default_resonance` to `brain/calibration.json` via the Desktop-facing schema (`nexo schema --json`), so NEXO Desktop's Preferences dialog renders a select with `Máximo` / `Alto (recomendado)` / `Medio` / `Bajo` automatically no Desktop release needed. `resolve_tier_for_caller` reads calibration first and falls back to the legacy `schedule.json` location. `nexo preferences --resonance` writes both. The UI control only affects interactive sessions (`nexo chat`, Desktop new conversation, interactive `nexo update`); crons and background processes stay pinned per caller in `resonance_map.py`.
21
+ Version `5.10.0` is the current packaged-runtime line: fixes the deep-sleep extract bloat that made Session 1 take ~57 minutes on some installs (new `bare_mode` on `run_automation_prompt` wires `claude --bare` for JSON-only extractor callers — ~4.3× faster per child, sourced from `ANTHROPIC_API_KEY` env or `~/.claude/anthropic-api-key.txt`). `caller=` is now **mandatory** on `run_automation_prompt` — no silent fallback; every automation subprocess traces back to a registered caller with a deliberate tier. Five personal scripts (`personal/email-monitor`, `personal/github-monitor`, `personal/post-x`, `personal/followup-runner`, `personal/orchestrator-v2`) joined the resonance map with tiers picked per caller based on what each one does. gbp/* marketing posts bumped from `medio` to `alto` (public-facing copy, quality first over speed). 65 legacy protocol debts bulk-resolved as part of the audit the patterns that generated them are structurally closed by mandatory `caller=` + unified session log + bare_mode.
22
+
23
+ Previously in `5.9.1`: adds `default_resonance` to `brain/calibration.json` via the Desktop-facing schema (`nexo schema --json`), so NEXO Desktop's Preferences dialog renders a select with `Máximo` / `Alto (recomendado)` / `Medio` / `Bajo` automatically — no Desktop release needed. `resolve_tier_for_caller` reads calibration first and falls back to the legacy `schedule.json` location. `nexo preferences --resonance` writes both. The UI control only affects interactive sessions (`nexo chat`, Desktop new conversation, interactive `nexo update`); crons and background processes stay pinned per caller in `resonance_map.py`.
22
24
 
23
25
  Previously in `5.9.0`: every Claude/Codex invocation now flows through a central **resonance map** and a **unified session log**. Four tiers (`MAXIMO` / `ALTO` / `MEDIO` / `BAJO`) each resolve to a concrete `(model, reasoning_effort)` pair per backend. User-facing callers (`nexo chat`, Desktop new conversation, interactive `nexo update`) honour the user's `default_resonance` preference; system-owned callers (deep-sleep, evolution, catchup, GBP posts, …) run at a fixed tier chosen per caller in `src/resonance_map.py` — the user's preference never downgrades a cron we decided needs `MAXIMO`. Unknown callers raise `UnregisteredCallerError`. Migration #41 adds `caller`, `session_type`, `started_at`, `ended_at`, `pid`, `resonance_tier` to `automation_runs`; interactive sessions record a row at spawn (with `ended_at=NULL`) and update it on close, so the Brain now has a single source of truth for every Claude/Codex call regardless of origin. New `nexo preferences --resonance` CLI. New MCP tools `nexo_session_log_create` / `nexo_session_log_close` let NEXO Desktop (which spawns `claude` directly from its TypeScript process) feed the same log.
24
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "5.9.1",
3
+ "version": "5.10.0",
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",
@@ -792,6 +792,52 @@ def _build_enforcement_system_prompt() -> str:
792
792
  return "\n".join(lines) if (must_rules or should_rules) else ""
793
793
 
794
794
 
795
+ _ANTHROPIC_API_KEY_SEARCH_PATHS = (
796
+ Path.home() / ".claude" / "anthropic-api-key.txt",
797
+ Path.home() / ".nexo" / "config" / "anthropic-api-key.txt",
798
+ )
799
+
800
+
801
+ def _resolve_anthropic_api_key() -> str:
802
+ """Locate an Anthropic API key for bare-mode invocations.
803
+
804
+ ``claude --bare`` skips macOS Keychain auth entirely, so the child
805
+ must find the API key in ``ANTHROPIC_API_KEY`` or via ``apiKeyHelper``.
806
+ Rather than forcing the operator to export the key in every shell, we
807
+ check the two conventional NEXO locations:
808
+
809
+ ~/.claude/anthropic-api-key.txt
810
+ ~/.nexo/config/anthropic-api-key.txt
811
+
812
+ Returns the trimmed key string, or "" if none is available. Callers
813
+ that want to run in bare mode must fall back to non-bare execution
814
+ when this returns empty.
815
+ """
816
+ env_key = os.environ.get("ANTHROPIC_API_KEY", "").strip()
817
+ if env_key:
818
+ return env_key
819
+ for path in _ANTHROPIC_API_KEY_SEARCH_PATHS:
820
+ try:
821
+ if path.is_file():
822
+ key = path.read_text().strip()
823
+ if key:
824
+ return key
825
+ except OSError:
826
+ continue
827
+ return ""
828
+
829
+
830
+ # Callers for which bare_mode=True is safe. The child's only allowed_tools
831
+ # must be file/grep/shell (no ``mcp__nexo__*``), otherwise --bare's opt-out
832
+ # of plugin sync / MCP bootstrap breaks the run. Extract/synthesize fit
833
+ # this profile: they read transcripts + shared-context and emit JSON, no
834
+ # NEXO tool calls.
835
+ BARE_MODE_SAFE_CALLERS: frozenset[str] = frozenset({
836
+ "deep-sleep/extract",
837
+ "deep-sleep/synthesize",
838
+ })
839
+
840
+
795
841
  def run_automation_prompt(
796
842
  prompt: str,
797
843
  *,
@@ -807,6 +853,7 @@ def run_automation_prompt(
807
853
  append_system_prompt: str = "",
808
854
  allowed_tools: str = "",
809
855
  extra_args: list[str] | tuple[str, ...] | None = None,
856
+ bare_mode: bool | None = None,
810
857
  ) -> subprocess.CompletedProcess:
811
858
  prefs = load_client_preferences()
812
859
  selected_backend = backend or resolve_automation_backend(preferences=prefs)
@@ -822,35 +869,40 @@ def run_automation_prompt(
822
869
  reasoning_effort = profile["reasoning_effort"]
823
870
  selected_backend = _resolve_available_backend(selected_backend, preferences=prefs)
824
871
 
825
- # Resonance map takes over model+effort decisions when the caller is
826
- # registered. Explicit model/effort arguments still win (required for
827
- # edge cases like the fallback JSON-conversion call inside extract.py
828
- # that asks a shorter/cheaper follow-up).
829
- resonance_tier = ""
830
- if caller and not model and not reasoning_effort:
831
- try:
832
- from resonance_map import (
833
- resolve_model_and_effort,
834
- resolve_tier_for_caller,
835
- UnregisteredCallerError,
836
- )
837
- user_default = ""
838
- if isinstance(prefs, dict):
839
- user_default = str(prefs.get("default_resonance") or "").strip()
840
- resonance_tier = resolve_tier_for_caller(
841
- caller, user_default=user_default or None
842
- )
843
- mapped_model, mapped_effort = resolve_model_and_effort(
844
- caller, selected_backend, user_default=user_default or None
845
- )
846
- if mapped_model:
847
- model = mapped_model
848
- if mapped_effort:
849
- reasoning_effort = mapped_effort
850
- except (ImportError, UnregisteredCallerError):
851
- # Unknown caller during a transitional release: fall back to
852
- # the legacy task_profile / model_defaults resolution below.
853
- pass
872
+ # Resonance map decides (model, effort) for every call. ``caller`` is
873
+ # MANDATORY every script that invokes the automation backend must be
874
+ # registered in src/resonance_map.py so its reasoning budget is a
875
+ # deliberate choice rather than an accident of the global default.
876
+ #
877
+ # Explicit ``model`` or ``reasoning_effort`` arguments still override
878
+ # the mapped value (required for edge cases like the fallback JSON-
879
+ # conversion call inside extract.py that runs on a shorter budget).
880
+ from resonance_map import (
881
+ resolve_model_and_effort,
882
+ resolve_tier_for_caller,
883
+ UnregisteredCallerError,
884
+ )
885
+ if not caller:
886
+ raise UnregisteredCallerError(
887
+ "run_automation_prompt requires caller=. Register a new entry in "
888
+ "src/resonance_map.py under USER_FACING_CALLERS or "
889
+ "SYSTEM_OWNED_CALLERS and pass the id here."
890
+ )
891
+ user_default = ""
892
+ if isinstance(prefs, dict):
893
+ user_default = str(prefs.get("default_resonance") or "").strip()
894
+ # This raises UnregisteredCallerError if caller is unknown — the
895
+ # same fail-closed rule we wanted. No silent fallback.
896
+ resonance_tier = resolve_tier_for_caller(
897
+ caller, user_default=user_default or None
898
+ )
899
+ mapped_model, mapped_effort = resolve_model_and_effort(
900
+ caller, selected_backend, user_default=user_default or None
901
+ )
902
+ if mapped_model and not model:
903
+ model = mapped_model
904
+ if mapped_effort and not reasoning_effort:
905
+ reasoning_effort = mapped_effort
854
906
 
855
907
  enforcement_fragment = _build_enforcement_system_prompt()
856
908
  if enforcement_fragment:
@@ -877,6 +929,39 @@ def run_automation_prompt(
877
929
  raise AutomationBackendUnavailableError(
878
930
  "Claude Code automation backend selected but `claude` is not installed."
879
931
  )
932
+
933
+ # bare_mode: when the caller only needs the model (no MCP tool
934
+ # calls, no CLAUDE.md context, no plugins) we pass Claude CLI's
935
+ # --bare flag. That skips hooks, LSP, plugin sync, CLAUDE.md
936
+ # auto-discovery, keychain reads, and background prefetches —
937
+ # reducing the per-invocation overhead from ~9s to ~2s on 2.1.x.
938
+ # Concretely: deep-sleep/extract was running 57min on Session 1
939
+ # because each extract attempt inherited ~15KB of NEXO CLAUDE.md
940
+ # system prompt it did not need. With --bare that extraction
941
+ # completes in seconds.
942
+ #
943
+ # Selection rules:
944
+ # - bare_mode=True explicit → trust the caller.
945
+ # - bare_mode=None (default) + caller in BARE_MODE_SAFE_CALLERS
946
+ # → auto-enable.
947
+ # - bare_mode=False → never.
948
+ # - --bare disables keychain auth, so we must provide an
949
+ # ANTHROPIC_API_KEY. If one cannot be located, fall back to
950
+ # normal mode with a warning on stderr rather than failing.
951
+ resolved_bare = False
952
+ if bare_mode is True:
953
+ resolved_bare = True
954
+ elif bare_mode is None and caller in BARE_MODE_SAFE_CALLERS:
955
+ resolved_bare = True
956
+
957
+ bare_api_key = ""
958
+ if resolved_bare:
959
+ bare_api_key = _resolve_anthropic_api_key()
960
+ if not bare_api_key:
961
+ # Silent fallback: we would rather take the slower path
962
+ # than force the caller to fail-closed on an env quirk.
963
+ resolved_bare = False
964
+
880
965
  # Headless claude -p does NOT reliably honour permissions.allow from
881
966
  # settings.json for MCP tool calls — it can stall waiting for an
882
967
  # approval that will never come in non-interactive mode. All NEXO
@@ -885,7 +970,15 @@ def run_automation_prompt(
885
970
  # so the process actually runs instead of zombying. Interactive
886
971
  # sessions (`nexo chat`) never go through this code path and keep
887
972
  # their normal approval prompts.
888
- cmd = [claude_bin, "-p", prompt, "--dangerously-skip-permissions"]
973
+ cmd = [claude_bin, "-p", prompt]
974
+ if resolved_bare:
975
+ cmd.append("--bare")
976
+ # Guarantee the child sees the API key regardless of how the
977
+ # parent's env was sanitized by _headless_env.
978
+ run_env = dict(run_env)
979
+ run_env["ANTHROPIC_API_KEY"] = bare_api_key
980
+ else:
981
+ cmd.append("--dangerously-skip-permissions")
889
982
  if resolved_model:
890
983
  cmd.extend(["--model", resolved_model])
891
984
  if resolved_effort:
@@ -143,14 +143,25 @@ SYSTEM_OWNED_CALLERS: dict[str, str] = {
143
143
  "tools/drive_search": "medio",
144
144
 
145
145
  # ---- Marketing automation ---------------------------------------------
146
- # These produce short copy; we could run them at BAJO for speed, but the
147
- # output is user-visible on a public surface, so we lean MEDIO for safety
148
- # against embarrassing outputs.
149
- "gbp/daily_post": "medio",
150
- "gbp/post_wazion": "medio",
151
- "gbp/post_psicologa": "medio",
152
- "gbp/monthly_audit": "medio",
153
- "gbp/reviews_watch": "medio",
146
+ # These post to Google Business Profile on behalf of Francisco's
147
+ # businesses. Short copy, but user-visible on a public surface; a
148
+ # mediocre post embarrasses the brand. Running them ALTO even though
149
+ # it's ~200 chars keeps the output quality tight.
150
+ "gbp/daily_post": "alto",
151
+ "gbp/post_wazion": "alto",
152
+ "gbp/post_psicologa": "alto",
153
+ "gbp/monthly_audit": "alto",
154
+ "gbp/reviews_watch": "alto",
155
+
156
+ # ---- Personal scripts (operators' own LaunchAgents) -------------------
157
+ # Francisco + Maria ship the same set of personal scripts via
158
+ # ~/.nexo/scripts (installed per-user, not through the core manifest).
159
+ # They all call into mcp__nexo__* so they cannot run under --bare.
160
+ "personal/email-monitor": "alto", # answer real user emails, quality matters
161
+ "personal/github-monitor": "alto", # reason about issues/PRs, not mechanical
162
+ "personal/post-x": "alto", # public-facing copy
163
+ "personal/followup-runner": "alto", # executes due followups, output is user-visible
164
+ "personal/orchestrator-v2": "maximo", # autonomous orchestration, critical reasoning
154
165
  }
155
166
 
156
167
  ALL_REGISTERED_CALLERS: frozenset[str] = frozenset(