nexo-brain 7.26.0 → 7.27.1

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.26.0",
3
+ "version": "7.27.1",
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.26.0` is the current packaged-runtime line. Minor release over v7.25.6 - provider runtime parity lets Desktop-managed Brain choose Anthropic Claude or OpenAI Codex, keep provider metadata on sessions/automation/crons, and provision the managed Codex runtime from bundled Desktop resources.
21
+ Version `7.27.1` is the current packaged-runtime line. Patch release over v7.27.0 - lifecycle stop calls now skip external provider session UUIDs safely, and provider runtime selection keeps chat plus automation aligned to the same Anthropic/OpenAI account.
22
+
23
+ Previously in `7.27.0`: minor release over v7.26.0 - Codex-side defaults move to verified `gpt-5.5` resonance tiers, managed config healing follows that model family, and Local Memory file-type actions tolerate transient SQLite locks.
24
+
25
+ Previously in `7.26.0`: minor release over v7.25.6 - provider runtime parity lets Desktop-managed Brain choose Anthropic or OpenAI, keep provider metadata on sessions/automation/crons, and provision the managed OpenAI runtime from bundled Desktop resources.
22
26
 
23
27
  Previously in `7.25.6`: patch release over v7.25.5 - existing Local Memory sidecar databases repair legacy root/exclusion columns before source-dependent indexes are created, and core background crons prefer the NEXO-managed Python runtime.
24
28
 
package/bin/nexo-brain.js CHANGED
@@ -115,8 +115,8 @@ const PUBLIC_CONTRIBUTION_UPSTREAM = "wazionapps/nexo";
115
115
  const MODEL_DEFAULTS_PATH = path.join(__dirname, "..", "src", "model_defaults.json");
116
116
  function _loadModelDefaults() {
117
117
  const fallback = {
118
- claude_code: { model: "claude-opus-4-6[1m]", reasoning_effort: "", display_name: "Opus 4.6 with 1M context" },
119
- codex: { model: "gpt-5.4", reasoning_effort: "xhigh", display_name: "GPT-5.4 with max reasoning" },
118
+ claude_code: { model: "claude-opus-4-7[1m]", reasoning_effort: "max", display_name: "Opus 4.7 with 1M context" },
119
+ codex: { model: "gpt-5.5", reasoning_effort: "xhigh", display_name: "GPT-5.5 with max reasoning" },
120
120
  };
121
121
  try {
122
122
  const raw = JSON.parse(fs.readFileSync(MODEL_DEFAULTS_PATH, "utf8"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.26.0",
3
+ "version": "7.27.1",
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",
@@ -44,6 +44,7 @@ CLAUDE_LEGACY_MODEL_HINTS = {"opus", "sonnet"}
44
44
  MODEL_PRICING_USD_PER_1M = {
45
45
  # Pricing snapshot used only when the backend does not return explicit cost.
46
46
  # Codex model names map to the current GPT-5 family pricing.
47
+ "gpt-5.5": {"input": 1.25, "cached_input": 0.125, "output": 10.0},
47
48
  "gpt-5.4": {"input": 1.25, "cached_input": 0.125, "output": 10.0},
48
49
  "gpt-5.4-mini": {"input": 0.25, "cached_input": 0.025, "output": 2.0},
49
50
  }
@@ -66,7 +67,8 @@ def _canonical_pricing_model(model: str) -> str:
66
67
  lowered = str(model or "").strip().lower()
67
68
  lowered = lowered.split("[", 1)[0]
68
69
  aliases = {
69
- "gpt-5": "gpt-5.4",
70
+ "gpt-5": "gpt-5.5",
71
+ "gpt-5.5": "gpt-5.5",
70
72
  "gpt-5.4": "gpt-5.4",
71
73
  "gpt-5-mini": "gpt-5.4-mini",
72
74
  "gpt-5.4-mini": "gpt-5.4-mini",
@@ -260,7 +260,7 @@ def call_model_raw(
260
260
  Parameters follow the Fase 2 plan doc 1 spec:
261
261
 
262
262
  prompt — the user-role text (English or the model's default).
263
- tier — resonance tier; default "muy_bajo" → Haiku / gpt-5.4-mini.
263
+ tier — resonance tier; default "muy_bajo" → Haiku / gpt-5.5 low.
264
264
  caller — resonance caller label. Must be registered in
265
265
  resonance_map.SYSTEM_OWNED_CALLERS. Default
266
266
  "enforcer_classifier".
package/src/cli.py CHANGED
@@ -2519,6 +2519,7 @@ def _preferences(args):
2519
2519
  PROVIDER_KEYS,
2520
2520
  PROVIDER_NONE,
2521
2521
  load_client_preferences,
2522
+ normalize_provider_key,
2522
2523
  normalize_default_terminal_client,
2523
2524
  save_client_preferences,
2524
2525
  )
@@ -2554,6 +2555,18 @@ def _preferences(args):
2554
2555
  file=sys.stderr,
2555
2556
  )
2556
2557
  return 2
2558
+ normalized_chat_provider = normalize_provider_key(chat_provider)
2559
+ normalized_automation_provider = normalize_provider_key(automation_provider)
2560
+ if (
2561
+ normalized_chat_provider in PROVIDER_KEYS
2562
+ and normalized_automation_provider in PROVIDER_KEYS
2563
+ and normalized_chat_provider != normalized_automation_provider
2564
+ ):
2565
+ print(
2566
+ "[NEXO] Chat and background automation must use the same provider.",
2567
+ file=sys.stderr,
2568
+ )
2569
+ return 2
2557
2570
 
2558
2571
  if args.resonance:
2559
2572
  tier = args.resonance.lower()
@@ -2668,12 +2681,16 @@ def _provider(args):
2668
2681
  if target not in PROVIDER_KEYS:
2669
2682
  print(f"[NEXO] Unknown provider '{target}'. Valid values: {', '.join(PROVIDER_KEYS)}.", file=sys.stderr)
2670
2683
  return 2
2671
- chat_provider = None if getattr(args, "automation_only", False) else target
2672
- automation_provider = None if getattr(args, "chat_only", False) else target
2684
+ if getattr(args, "automation_only", False) or getattr(args, "chat_only", False):
2685
+ print(
2686
+ "[NEXO] Provider selection is unified: chat and background automation use the same provider.",
2687
+ file=sys.stderr,
2688
+ )
2689
+ return 2
2673
2690
  save_client_preferences(
2674
- selected_chat_provider=chat_provider,
2675
- automation_provider=automation_provider,
2676
- automation_user_override=True if automation_provider is not None else None,
2691
+ selected_chat_provider=target,
2692
+ automation_provider=target,
2693
+ automation_user_override=True,
2677
2694
  )
2678
2695
  prefs = load_client_preferences()
2679
2696
  provider_runtime = prefs.get("provider_runtime") if isinstance(prefs.get("provider_runtime"), dict) else {}
@@ -203,12 +203,15 @@ def _managed_codex_vendor_present(home: Path | None = None) -> bool:
203
203
  managed_prefix / "node_modules" / "@openai" / "codex",
204
204
  )
205
205
  for package_root in package_roots:
206
- vendor_root = package_root / "vendor"
207
- if not vendor_root.exists():
208
- continue
206
+ vendor_roots = [package_root / "vendor"]
207
+ optional_root = package_root / "node_modules" / "@openai"
208
+ if optional_root.is_dir():
209
+ vendor_roots.extend(item / "vendor" for item in optional_root.glob("codex-*"))
210
+ existing_vendor_roots = [item for item in vendor_roots if item.exists()]
209
211
  try:
210
- if any(candidate.is_file() for candidate in vendor_root.rglob("bin/codex*")):
211
- return True
212
+ for vendor_root in existing_vendor_roots:
213
+ if any(candidate.is_file() for candidate in vendor_root.rglob("bin/codex*")):
214
+ return True
212
215
  except Exception:
213
216
  continue
214
217
  return False
@@ -370,6 +373,21 @@ def _provider_from_runtime_payload(value, key: str, fallback_client: str) -> str
370
373
  return client_to_provider(fallback_client) or PROVIDER_ANTHROPIC
371
374
 
372
375
 
376
+ def _provider_runtime_is_unselected_default(value) -> bool:
377
+ if not isinstance(value, dict):
378
+ return False
379
+ selected = normalize_provider_key(value.get("selected_chat_provider"))
380
+ automation_provider = normalize_provider_key(value.get("automation_provider"))
381
+ automation_backend = normalize_backend_key(value.get("automation_backend"))
382
+ last_change = value.get("last_provider_change") if isinstance(value.get("last_provider_change"), dict) else {}
383
+ return (
384
+ selected == PROVIDER_ANTHROPIC
385
+ and automation_provider in {"", PROVIDER_ANTHROPIC}
386
+ and automation_backend in {"", CLIENT_CLAUDE_CODE}
387
+ and not last_change.get("changed_at")
388
+ )
389
+
390
+
373
391
  def normalize_provider_runtime(
374
392
  value,
375
393
  *,
@@ -385,19 +403,17 @@ def normalize_provider_runtime(
385
403
  default_terminal_client,
386
404
  )
387
405
  raw_automation_provider = normalize_provider_key(raw.get("automation_provider")) if isinstance(raw, dict) else ""
388
- automation_provider = (
389
- PROVIDER_NONE
390
- if raw_automation_provider == PROVIDER_NONE
391
- else _provider_from_runtime_payload(
392
- raw,
393
- "automation_provider",
394
- automation_backend if automation_enabled else BACKEND_NONE,
395
- )
396
- )
397
- if not automation_enabled or automation_backend == BACKEND_NONE:
406
+ raw_has_automation_backend = isinstance(raw, dict) and "automation_backend" in raw
407
+ raw_automation_backend = normalize_backend_key(raw.get("automation_backend")) if raw_has_automation_backend else ""
408
+ if (
409
+ not automation_enabled
410
+ or automation_backend == BACKEND_NONE
411
+ or raw_automation_provider == PROVIDER_NONE
412
+ or (raw_has_automation_backend and raw_automation_backend == BACKEND_NONE)
413
+ ):
398
414
  automation_provider = PROVIDER_NONE
399
- elif automation_provider not in PROVIDER_KEYS:
400
- automation_provider = client_to_provider(automation_backend) or PROVIDER_ANTHROPIC
415
+ else:
416
+ automation_provider = selected_provider
401
417
 
402
418
  providers = defaults["providers"]
403
419
  raw_providers = raw.get("providers") if isinstance(raw.get("providers"), dict) else {}
@@ -689,8 +705,19 @@ def normalize_client_preferences(
689
705
  runtime_profiles = normalize_client_runtime_profiles(
690
706
  schedule.get("client_runtime_profiles")
691
707
  )
708
+ raw_provider_runtime = schedule.get("provider_runtime")
709
+ legacy_terminal_provider = client_to_provider(default_terminal_client)
710
+ if (
711
+ legacy_terminal_provider in PROVIDER_KEYS
712
+ and legacy_terminal_provider != PROVIDER_ANTHROPIC
713
+ and _provider_runtime_is_unselected_default(raw_provider_runtime)
714
+ ):
715
+ raw_provider_runtime = dict(raw_provider_runtime or {})
716
+ raw_provider_runtime["selected_chat_provider"] = legacy_terminal_provider
717
+ raw_provider_runtime["automation_provider"] = legacy_terminal_provider
718
+ raw_provider_runtime["automation_backend"] = provider_to_client(legacy_terminal_provider)
692
719
  provider_runtime = normalize_provider_runtime(
693
- schedule.get("provider_runtime"),
720
+ raw_provider_runtime,
694
721
  default_terminal_client=default_terminal_client,
695
722
  automation_backend=automation_backend,
696
723
  automation_enabled=automation_enabled,
@@ -783,28 +810,53 @@ def apply_client_preferences(
783
810
  else current.get("provider_runtime")
784
811
  )
785
812
  raw_provider_runtime = dict(raw_provider_runtime or {})
786
- if selected_chat_provider is not None:
787
- raw_provider_runtime["selected_chat_provider"] = selected_chat_provider
788
- derived_chat_client = provider_to_client(selected_chat_provider)
789
- if derived_chat_client:
790
- merged["interactive_clients"][derived_chat_client] = True
791
- merged["default_terminal_client"] = derived_chat_client
813
+ explicit_selected_provider = normalize_provider_key(selected_chat_provider)
814
+ explicit_automation_provider = normalize_provider_key(automation_provider)
815
+ backend_provider = (
816
+ client_to_provider(merged["automation_backend"])
817
+ if merged["automation_backend"] != BACKEND_NONE
818
+ else ""
819
+ )
820
+ terminal_provider = (
821
+ client_to_provider(merged["default_terminal_client"])
822
+ if default_terminal_client is not None
823
+ else ""
824
+ )
825
+ single_provider = (
826
+ (explicit_selected_provider if explicit_selected_provider != PROVIDER_NONE else "")
827
+ or (explicit_automation_provider if explicit_automation_provider != PROVIDER_NONE else "")
828
+ or (backend_provider if automation_backend is not None else "")
829
+ or terminal_provider
830
+ )
831
+ if single_provider in PROVIDER_KEYS:
832
+ raw_provider_runtime["selected_chat_provider"] = single_provider
833
+ selected_client = provider_to_client(single_provider)
834
+ if selected_client:
835
+ merged["interactive_clients"][selected_client] = True
836
+ merged["default_terminal_client"] = selected_client
792
837
  elif default_terminal_client is not None:
793
838
  raw_provider_runtime["selected_chat_provider"] = client_to_provider(
794
839
  merged["default_terminal_client"]
795
840
  )
796
- if automation_provider is not None:
797
- raw_provider_runtime["automation_provider"] = automation_provider
798
- derived_backend = provider_to_client(automation_provider)
799
- if derived_backend:
800
- merged["interactive_clients"][derived_backend] = True
801
- merged["automation_backend"] = (
802
- derived_backend if derived_backend else BACKEND_NONE
803
- )
841
+
842
+ automation_off = (
843
+ merged["automation_enabled"] is False
844
+ or merged["automation_backend"] == BACKEND_NONE
845
+ or explicit_automation_provider == PROVIDER_NONE
846
+ )
847
+ if automation_off:
848
+ raw_provider_runtime["automation_provider"] = PROVIDER_NONE
849
+ raw_provider_runtime["automation_backend"] = BACKEND_NONE
850
+ merged["automation_backend"] = BACKEND_NONE
851
+ elif single_provider in PROVIDER_KEYS:
852
+ raw_provider_runtime["automation_provider"] = single_provider
853
+ merged["automation_backend"] = provider_to_client(single_provider)
854
+ raw_provider_runtime["automation_backend"] = merged["automation_backend"]
804
855
  elif automation_backend is not None or automation_enabled is not None:
805
856
  raw_provider_runtime["automation_provider"] = client_to_provider(
806
857
  merged["automation_backend"]
807
858
  ) if merged["automation_backend"] != BACKEND_NONE else PROVIDER_NONE
859
+ raw_provider_runtime["automation_backend"] = merged["automation_backend"]
808
860
  merged["provider_runtime"] = normalize_provider_runtime(
809
861
  raw_provider_runtime,
810
862
  default_terminal_client=merged["default_terminal_client"],
@@ -974,7 +1026,7 @@ def detect_installed_clients(user_home: str | os.PathLike[str] | None = None) ->
974
1026
 
975
1027
 
976
1028
  def resolve_terminal_client(requested: str | None = None, *, preferences: dict | None = None) -> str:
977
- normalized = preferences or load_client_preferences()
1029
+ normalized = normalize_client_preferences(preferences) if preferences is not None else load_client_preferences()
978
1030
  if requested is not None:
979
1031
  interactive_clients = normalized["interactive_clients"]
980
1032
  return normalize_default_terminal_client(requested, interactive_clients=interactive_clients)
@@ -985,7 +1037,7 @@ def resolve_terminal_client(requested: str | None = None, *, preferences: dict |
985
1037
 
986
1038
 
987
1039
  def resolve_selected_chat_provider(preferences: dict | None = None) -> str:
988
- normalized = preferences or load_client_preferences()
1040
+ normalized = normalize_client_preferences(preferences) if preferences is not None else load_client_preferences()
989
1041
  provider_runtime = normalize_provider_runtime(
990
1042
  normalized.get("provider_runtime"),
991
1043
  default_terminal_client=normalized.get("default_terminal_client", CLIENT_CLAUDE_CODE),
@@ -996,7 +1048,7 @@ def resolve_selected_chat_provider(preferences: dict | None = None) -> str:
996
1048
 
997
1049
 
998
1050
  def resolve_automation_provider(preferences: dict | None = None) -> str:
999
- normalized = preferences or load_client_preferences()
1051
+ normalized = normalize_client_preferences(preferences) if preferences is not None else load_client_preferences()
1000
1052
  provider_runtime = normalize_provider_runtime(
1001
1053
  normalized.get("provider_runtime"),
1002
1054
  default_terminal_client=normalized.get("default_terminal_client", CLIENT_CLAUDE_CODE),
@@ -1013,7 +1065,7 @@ def resolve_user_model(preferences: dict | None = None) -> str:
1013
1065
  string. The value comes from the user's ``client_runtime_profiles`` for
1014
1066
  their ``default_terminal_client`` (usually ``claude_code``).
1015
1067
  """
1016
- normalized = preferences or load_client_preferences()
1068
+ normalized = normalize_client_preferences(preferences) if preferences is not None else load_client_preferences()
1017
1069
  client = normalize_default_terminal_client(
1018
1070
  normalized["default_terminal_client"],
1019
1071
  interactive_clients=normalized["interactive_clients"],
@@ -1023,11 +1075,8 @@ def resolve_user_model(preferences: dict | None = None) -> str:
1023
1075
 
1024
1076
 
1025
1077
  def resolve_automation_backend(preferences: dict | None = None) -> str:
1026
- normalized = preferences or load_client_preferences()
1027
- return normalize_automation_backend(
1028
- normalized["automation_backend"],
1029
- automation_enabled=normalized["automation_enabled"],
1030
- )
1078
+ normalized = normalize_client_preferences(preferences) if preferences is not None else load_client_preferences()
1079
+ return normalized["automation_backend"]
1031
1080
 
1032
1081
 
1033
1082
  def resolve_client_runtime_profile(
@@ -65,10 +65,9 @@ except Exception:
65
65
  }
66
66
 
67
67
  def resolve_client_runtime_profile(client: str, preferences: dict | None = None) -> dict:
68
- _default_model = "claude-opus-4-7[1m]"
69
68
  defaults = {
70
- "claude_code": {"model": _default_model, "reasoning_effort": ""},
71
- "codex": {"model": _default_model, "reasoning_effort": ""},
69
+ "claude_code": {"model": "claude-opus-4-7[1m]", "reasoning_effort": "max"},
70
+ "codex": {"model": "gpt-5.5", "reasoning_effort": "xhigh"},
72
71
  }
73
72
  return dict(defaults.get(client, {}))
74
73
 
@@ -241,6 +240,15 @@ def _brain_bundle_root() -> Path:
241
240
  return Path(__file__).resolve().parents[1]
242
241
 
243
242
 
243
+ def _codex_bundle_dir() -> Path:
244
+ explicit = str(os.environ.get("NEXO_CODEX_BUNDLE_DIR", "")).strip()
245
+ if explicit:
246
+ candidate = Path(explicit).expanduser()
247
+ if candidate.is_dir():
248
+ return candidate
249
+ return _brain_bundle_root() / "codex"
250
+
251
+
244
252
  def _platform_slug() -> str:
245
253
  if sys.platform.startswith("darwin"):
246
254
  os_part = "darwin"
@@ -379,12 +387,15 @@ def _codex_vendor_present(managed_prefix: Path) -> bool:
379
387
  managed_prefix / "node_modules" / "@openai" / "codex",
380
388
  ]
381
389
  for package_root in package_roots:
382
- vendor_root = package_root / "vendor"
383
- if not vendor_root.exists():
384
- continue
390
+ vendor_roots = [package_root / "vendor"]
391
+ optional_root = package_root / "node_modules" / "@openai"
392
+ if optional_root.is_dir():
393
+ vendor_roots.extend(item / "vendor" for item in optional_root.glob("codex-*"))
394
+ existing_vendor_roots = [item for item in vendor_roots if item.exists()]
385
395
  try:
386
- if any(candidate.is_file() for candidate in vendor_root.rglob("bin/codex*")):
387
- return True
396
+ for vendor_root in existing_vendor_roots:
397
+ if any(candidate.is_file() for candidate in vendor_root.rglob("bin/codex*")):
398
+ return True
388
399
  except Exception:
389
400
  continue
390
401
  return False
@@ -659,7 +670,7 @@ def ensure_codex_installed(*, user_home: str | os.PathLike[str] | None = None) -
659
670
  )
660
671
 
661
672
  desktop_node, bundled_npm_cli = _bundled_npm_runtime()
662
- bundle_dir = _brain_bundle_root() / "codex"
673
+ bundle_dir = _codex_bundle_dir()
663
674
  if desktop_managed and not (desktop_node and bundled_npm_cli):
664
675
  vendor_installed = _install_codex_vendor_from_bundle(
665
676
  bundle_dir=bundle_dir,
@@ -946,7 +957,7 @@ def _sync_codex_managed_config(
946
957
  # Heal any pre-existing Claude model written by earlier NEXO versions.
947
958
  existing_model = str(payload.get("model") or "").strip()
948
959
  if existing_model and _looks_like_claude_model(existing_model):
949
- payload["model"] = "gpt-5.4"
960
+ payload["model"] = "gpt-5.5"
950
961
 
951
962
  # Only write a model from the runtime profile into Codex config if it
952
963
  # looks like a Codex/OpenAI model. Claude models are invalid for Codex
@@ -956,7 +967,7 @@ def _sync_codex_managed_config(
956
967
  payload["model"] = profile_model
957
968
  elif profile_model:
958
969
  # Fall back to a known-good Codex default to self-heal.
959
- payload["model"] = "gpt-5.4"
970
+ payload["model"] = "gpt-5.5"
960
971
  if "reasoning_effort" in runtime_profile:
961
972
  payload["model_reasoning_effort"] = runtime_profile.get("reasoning_effort") or ""
962
973
 
@@ -121,7 +121,7 @@ def classify(
121
121
  context: Optional extra context appended to the user message.
122
122
  call_raw: Injection point for tests — defaults to call_model_raw.
123
123
  cache: TTL cache instance. Tests can pass a fresh cache.
124
- tier: Resonance tier. Default "muy_bajo" (Haiku / gpt-5.4-mini).
124
+ tier: Resonance tier. Default "muy_bajo" (Haiku / gpt-5.5 low).
125
125
  tristate: When True, return "yes" / "no" / "unknown" as strings.
126
126
  "unknown" represents the conservative-parse-fallback path
127
127
  which existing bool callers cannot distinguish from a real
@@ -383,18 +383,22 @@ def _effective_file_type_rule(conn, extension: str) -> dict:
383
383
 
384
384
 
385
385
  def list_file_type_rules(*, readonly: bool = True) -> dict:
386
- if not readonly:
386
+ def _list() -> dict:
387
+ if readonly:
388
+ conn = _read_conn()
389
+ try:
390
+ rows = _list_file_type_rules_conn(conn)
391
+ finally:
392
+ _close_read_conn(conn)
393
+ return _shape_file_type_rules(rows)
394
+
387
395
  conn = _conn()
388
396
  seed_core_file_type_rules(conn)
389
397
  conn.commit()
390
398
  rows = _list_file_type_rules_conn(conn)
391
- else:
392
- conn = _read_conn()
393
- try:
394
- rows = _list_file_type_rules_conn(conn)
395
- finally:
396
- _close_read_conn(conn)
397
- return _shape_file_type_rules(rows)
399
+ return _shape_file_type_rules(rows)
400
+
401
+ return _with_sqlite_busy_retry(_list)
398
402
 
399
403
 
400
404
  def _purge_assets_by_extension(conn, extension: str) -> dict:
@@ -406,49 +410,58 @@ def _purge_assets_by_extension(conn, extension: str) -> dict:
406
410
 
407
411
 
408
412
  def set_file_type_rule(extension: str, *, action: str = "extract", source: str = "user", priority: int | None = None, reason: str = "user") -> dict:
409
- conn = _conn()
410
- ext = _normalize_extension(extension)
411
- if not ext:
412
- return {"ok": False, "error": "extension_required"}
413
- normalized_action = _normalize_file_type_action(action)
414
- source_value = _normalize_source(source)
415
- priority_value = int(priority if priority is not None else (82 if normalized_action == "extract" else 20 if normalized_action == "metadata" else 0))
416
- timestamp = now()
417
- conn.execute(
418
- """
419
- INSERT INTO local_index_file_type_rules(extension, action, source, priority, reason, created_at, updated_at)
420
- VALUES (?, ?, ?, ?, ?, ?, ?)
421
- ON CONFLICT(extension, source) DO UPDATE SET
422
- action=excluded.action,
423
- priority=excluded.priority,
424
- reason=excluded.reason,
425
- updated_at=excluded.updated_at
426
- """,
427
- (ext, normalized_action, source_value, priority_value, reason, timestamp, timestamp),
428
- )
429
- cleanup = _purge_assets_by_extension(conn, ext) if normalized_action == "ignore" and source_value == "user" else {"assets": 0}
430
- conn.commit()
431
- log_event("info", "file_type_rule_set", "Local memory file type rule set", extension=ext, action=normalized_action, source=source_value, cleanup=cleanup)
432
- return {"ok": True, "extension": ext, "action": normalized_action, "source": source_value, "priority": priority_value, "cleanup": cleanup}
413
+ def _set() -> dict:
414
+ conn = _conn()
415
+ ext = _normalize_extension(extension)
416
+ if not ext:
417
+ return {"ok": False, "error": "extension_required"}
418
+ normalized_action = _normalize_file_type_action(action)
419
+ source_value = _normalize_source(source)
420
+ priority_value = int(priority if priority is not None else (82 if normalized_action == "extract" else 20 if normalized_action == "metadata" else 0))
421
+ timestamp = now()
422
+ conn.execute(
423
+ """
424
+ INSERT INTO local_index_file_type_rules(extension, action, source, priority, reason, created_at, updated_at)
425
+ VALUES (?, ?, ?, ?, ?, ?, ?)
426
+ ON CONFLICT(extension, source) DO UPDATE SET
427
+ action=excluded.action,
428
+ priority=excluded.priority,
429
+ reason=excluded.reason,
430
+ updated_at=excluded.updated_at
431
+ """,
432
+ (ext, normalized_action, source_value, priority_value, reason, timestamp, timestamp),
433
+ )
434
+ cleanup = _purge_assets_by_extension(conn, ext) if normalized_action == "ignore" and source_value == "user" else {"assets": 0}
435
+ conn.commit()
436
+ log_event("info", "file_type_rule_set", "Local memory file type rule set", extension=ext, action=normalized_action, source=source_value, cleanup=cleanup)
437
+ return {"ok": True, "extension": ext, "action": normalized_action, "source": source_value, "priority": priority_value, "cleanup": cleanup}
438
+
439
+ return _with_sqlite_busy_retry(_set)
433
440
 
434
441
 
435
442
  def remove_file_type_rule(extension: str, *, source: str = "user") -> dict:
436
- conn = _conn()
437
- ext = _normalize_extension(extension)
438
- source_value = _normalize_source(source)
439
- conn.execute("DELETE FROM local_index_file_type_rules WHERE extension=? AND source=?", (ext, source_value))
440
- conn.commit()
441
- log_event("info", "file_type_rule_removed", "Local memory file type rule removed", extension=ext, source=source_value)
442
- return {"ok": True, "extension": ext, "source": source_value}
443
+ def _remove() -> dict:
444
+ conn = _conn()
445
+ ext = _normalize_extension(extension)
446
+ source_value = _normalize_source(source)
447
+ conn.execute("DELETE FROM local_index_file_type_rules WHERE extension=? AND source=?", (ext, source_value))
448
+ conn.commit()
449
+ log_event("info", "file_type_rule_removed", "Local memory file type rule removed", extension=ext, source=source_value)
450
+ return {"ok": True, "extension": ext, "source": source_value}
451
+
452
+ return _with_sqlite_busy_retry(_remove)
443
453
 
444
454
 
445
455
  def reset_file_type_rules() -> dict:
446
- conn = _conn()
447
- deleted = int(conn.execute("DELETE FROM local_index_file_type_rules WHERE source='user'").rowcount or 0)
448
- seeded = seed_core_file_type_rules(conn)
449
- conn.commit()
450
- log_event("info", "file_type_rules_reset", "Local memory user file type overrides reset", deleted=deleted)
451
- return {"ok": True, "deleted": deleted, "core_rules": int(seeded.get("rules") or 0), "file_types": list_file_type_rules(readonly=False)}
456
+ def _reset() -> dict:
457
+ conn = _conn()
458
+ deleted = int(conn.execute("DELETE FROM local_index_file_type_rules WHERE source='user'").rowcount or 0)
459
+ seeded = seed_core_file_type_rules(conn)
460
+ conn.commit()
461
+ log_event("info", "file_type_rules_reset", "Local memory user file type overrides reset", deleted=deleted)
462
+ return {"ok": True, "deleted": deleted, "core_rules": int(seeded.get("rules") or 0), "file_types": list_file_type_rules(readonly=False)}
463
+
464
+ return _with_sqlite_busy_retry(_reset)
452
465
 
453
466
 
454
467
  def _file_type_action(conn, path: str | Path) -> str:
@@ -8,10 +8,10 @@
8
8
  "previous_defaults": ["claude-opus-4-6[1m]"]
9
9
  },
10
10
  "codex": {
11
- "model": "gpt-5.4",
11
+ "model": "gpt-5.5",
12
12
  "reasoning_effort": "xhigh",
13
- "display_name": "GPT-5.4 with max reasoning",
14
- "recommendation_version": 1,
15
- "previous_defaults": []
13
+ "display_name": "GPT-5.5 with max reasoning",
14
+ "recommendation_version": 2,
15
+ "previous_defaults": ["gpt-5.4"]
16
16
  }
17
17
  }
@@ -27,11 +27,11 @@ _FALLBACK: dict[str, Any] = {
27
27
  "previous_defaults": ["claude-opus-4-6[1m]"],
28
28
  },
29
29
  "codex": {
30
- "model": "gpt-5.4",
30
+ "model": "gpt-5.5",
31
31
  "reasoning_effort": "xhigh",
32
- "display_name": "GPT-5.4 with max reasoning",
33
- "recommendation_version": 1,
34
- "previous_defaults": [],
32
+ "display_name": "GPT-5.5 with max reasoning",
33
+ "recommendation_version": 2,
34
+ "previous_defaults": ["gpt-5.4"],
35
35
  },
36
36
  }
37
37
 
@@ -218,7 +218,15 @@ def handle_nexo_lifecycle_stop_nexo_session(
218
218
  from tools_sessions import handle_stop
219
219
 
220
220
  raw_sid = str(sid or "").strip()
221
- stop_sids = lifecycle_events.registered_stop_session_ids(raw_sid) or [raw_sid]
221
+ stop_sids = lifecycle_events.registered_stop_session_ids(raw_sid)
222
+ if not stop_sids:
223
+ return json.dumps({
224
+ "status": "ok",
225
+ "sid": raw_sid,
226
+ "stopped_session_ids": [],
227
+ "skipped": True,
228
+ "reason": "no-registered-nexo-session",
229
+ }, ensure_ascii=False)
222
230
  messages = [handle_stop(stop_sid) for stop_sid in stop_sids]
223
231
  return json.dumps({
224
232
  "status": "ok",
@@ -2,23 +2,23 @@
2
2
  "tiers": {
3
3
  "maximo": {
4
4
  "claude_code": { "model": "claude-opus-4-7[1m]", "effort": "max" },
5
- "codex": { "model": "gpt-5.4", "effort": "xhigh" }
5
+ "codex": { "model": "gpt-5.5", "effort": "xhigh" }
6
6
  },
7
7
  "alto": {
8
8
  "claude_code": { "model": "claude-opus-4-7[1m]", "effort": "xhigh" },
9
- "codex": { "model": "gpt-5.4", "effort": "high" }
9
+ "codex": { "model": "gpt-5.5", "effort": "high" }
10
10
  },
11
11
  "medio": {
12
12
  "claude_code": { "model": "claude-opus-4-7[1m]", "effort": "high" },
13
- "codex": { "model": "gpt-5.4", "effort": "medium" }
13
+ "codex": { "model": "gpt-5.5", "effort": "medium" }
14
14
  },
15
15
  "bajo": {
16
16
  "claude_code": { "model": "claude-opus-4-7[1m]", "effort": "medium" },
17
- "codex": { "model": "gpt-5.4", "effort": "low" }
17
+ "codex": { "model": "gpt-5.5", "effort": "low" }
18
18
  },
19
19
  "muy_bajo": {
20
20
  "claude_code": { "model": "claude-haiku-4-5-20251001", "effort": "" },
21
- "codex": { "model": "gpt-5.4-mini", "effort": "low" }
21
+ "codex": { "model": "gpt-5.5", "effort": "low" }
22
22
  }
23
23
  },
24
24
  "default_tier": "alto"
@@ -337,6 +337,23 @@ def _schedule_defaults() -> dict:
337
337
  "default_terminal_client": "claude_code",
338
338
  "automation_enabled": True,
339
339
  "automation_backend": "claude_code",
340
+ "provider_runtime": {
341
+ "schema_version": 1,
342
+ "selected_chat_provider": "anthropic",
343
+ "automation_provider": "anthropic",
344
+ "automation_backend": "claude_code",
345
+ "providers": {
346
+ "anthropic": {"client": "claude_code"},
347
+ "openai": {"client": "codex"},
348
+ },
349
+ "fallback_policy": {"chat": "ask", "automation": "fail_closed"},
350
+ "last_provider_change": {
351
+ "changed_at": None,
352
+ "from_provider": None,
353
+ "to_provider": None,
354
+ "source": None,
355
+ },
356
+ },
340
357
  "client_runtime_profiles": {
341
358
  "claude_code": {
342
359
  "model": DEFAULT_CLAUDE_CODE_MODEL,
@@ -361,16 +378,32 @@ def _schedule_defaults() -> dict:
361
378
  }
362
379
 
363
380
 
364
- def load_schedule_config() -> dict:
365
- if not SCHEDULE_FILE.is_file():
366
- return _schedule_defaults()
381
+ def _load_schedule_payload(path: Path) -> dict | None:
367
382
  try:
368
- data = json.loads(SCHEDULE_FILE.read_text())
383
+ data = json.loads(path.read_text())
369
384
  except Exception:
385
+ return None
386
+ return data if isinstance(data, dict) else None
387
+
388
+
389
+ def _legacy_schedule_file() -> Path:
390
+ return NEXO_HOME / "config" / "schedule.json"
391
+
392
+
393
+ def load_schedule_config() -> dict:
394
+ data: dict | None = None
395
+ if SCHEDULE_FILE.is_file():
396
+ data = _load_schedule_payload(SCHEDULE_FILE)
397
+ else:
398
+ legacy_file = _legacy_schedule_file()
399
+ if legacy_file != SCHEDULE_FILE and legacy_file.is_file():
400
+ data = _load_schedule_payload(legacy_file)
401
+ if data is None:
370
402
  return _schedule_defaults()
371
- if not isinstance(data, dict):
372
- return _schedule_defaults()
373
- merged = _schedule_defaults()
403
+ defaults = _schedule_defaults()
404
+ if "provider_runtime" not in data:
405
+ defaults.pop("provider_runtime", None)
406
+ merged = defaults
374
407
  merged.update(data)
375
408
  merged.setdefault("processes", {})
376
409
  return merged
@@ -63,11 +63,33 @@ for candidate in (
63
63
  except Exception:
64
64
  continue
65
65
 
66
- provider_runtime = schedule.get("provider_runtime") if isinstance(schedule.get("provider_runtime"), dict) else {}
67
- backend = str(provider_runtime.get("automation_backend") or schedule.get("automation_backend") or "claude_code").strip().lower()
68
- provider = str(provider_runtime.get("automation_provider") or "").strip().lower()
69
- if provider not in {"anthropic", "openai", "none"}:
70
- provider = {"claude_code": "anthropic", "codex": "openai", "none": "none"}.get(backend, "")
66
+ for import_root in (
67
+ nexo_home / "core",
68
+ nexo_home / "core" / "src",
69
+ nexo_home / "src",
70
+ ):
71
+ if import_root.exists():
72
+ sys.path.insert(0, str(import_root))
73
+ try:
74
+ from client_preferences import normalize_client_preferences # type: ignore
75
+ prefs = normalize_client_preferences(schedule)
76
+ provider_runtime = prefs.get("provider_runtime") if isinstance(prefs.get("provider_runtime"), dict) else {}
77
+ provider = str(provider_runtime.get("automation_provider") or "none").strip().lower()
78
+ backend = str(prefs.get("automation_backend") or provider_runtime.get("automation_backend") or "none").strip().lower()
79
+ except Exception:
80
+ provider_runtime = schedule.get("provider_runtime") if isinstance(schedule.get("provider_runtime"), dict) else {}
81
+ selected = str(provider_runtime.get("selected_chat_provider") or "").strip().lower()
82
+ backend_raw = str(provider_runtime.get("automation_backend") or schedule.get("automation_backend") or "claude_code").strip().lower()
83
+ provider_raw = str(provider_runtime.get("automation_provider") or "").strip().lower()
84
+ automation_enabled = schedule.get("automation_enabled", True) is not False
85
+ backend_map = {"claude_code": "anthropic", "codex": "openai", "none": "none"}
86
+ provider_map = {"anthropic": "anthropic", "openai": "openai", "none": "none"}
87
+ if (not automation_enabled) or backend_raw in {"none", "off", "disabled", "false", "0"} or provider_raw in {"none", "off", "disabled", "false", "0"}:
88
+ provider = "none"
89
+ backend = "none"
90
+ else:
91
+ provider = provider_map.get(selected) or provider_map.get(provider_raw) or backend_map.get(backend_raw, "anthropic")
92
+ backend = {"anthropic": "claude_code", "openai": "codex", "none": "none"}.get(provider, "claude_code")
71
93
  snapshot = {
72
94
  "selected_chat_provider": provider_runtime.get("selected_chat_provider") or "",
73
95
  "automation_provider": provider,
@@ -37,7 +37,7 @@ Each plist calls `/usr/bin/python3`. If your Python 3 is elsewhere (e.g. Homebre
37
37
 
38
38
  ### 3. Create required directories
39
39
 
40
- The agents write logs to `{{NEXO_HOME}}/logs/` and `{{NEXO_HOME}}/coordination/`. Create them if they do not exist:
40
+ The agents write logs to `{{NEXO_HOME}}/runtime/logs/` and `{{NEXO_HOME}}/coordination/`. Create them if they do not exist:
41
41
 
42
42
  ```bash
43
43
  mkdir -p "$NEXO_HOME/logs" "$NEXO_HOME/coordination"
@@ -117,7 +117,7 @@ These agents power NEXO's learning and memory systems. Strongly recommended.
117
117
 
118
118
  ## Logs
119
119
 
120
- All agents write stdout and stderr to files under `{{NEXO_HOME}}/logs/` (or `{{NEXO_HOME}}/coordination/` for the session-related ones). Check these first when debugging:
120
+ All agents write stdout and stderr to files under `{{NEXO_HOME}}/runtime/logs/` (or `{{NEXO_HOME}}/coordination/` for the session-related ones). Check these first when debugging:
121
121
 
122
122
  ```bash
123
123
  tail -50 "$NEXO_HOME/logs/watchdog-stdout.log"
@@ -14,7 +14,7 @@
14
14
  <key>ProgramArguments</key>
15
15
  <array>
16
16
  <string>/usr/bin/python3</string>
17
- <string>{{NEXO_HOME}}/auto_close_sessions.py</string>
17
+ <string>{{NEXO_HOME}}/core/auto_close_sessions.py</string>
18
18
  </array>
19
19
  <key>StartInterval</key>
20
20
  <integer>300</integer>
@@ -14,16 +14,16 @@
14
14
  <key>ProgramArguments</key>
15
15
  <array>
16
16
  <string>/usr/bin/python3</string>
17
- <string>{{NEXO_HOME}}/scripts/nexo-catchup.py</string>
17
+ <string>{{NEXO_HOME}}/core/scripts/nexo-catchup.py</string>
18
18
  </array>
19
19
  <key>RunAtLoad</key>
20
20
  <true/>
21
21
  <key>StartInterval</key>
22
22
  <integer>900</integer>
23
23
  <key>StandardOutPath</key>
24
- <string>{{NEXO_HOME}}/logs/catchup-stdout.log</string>
24
+ <string>{{NEXO_HOME}}/runtime/logs/catchup-stdout.log</string>
25
25
  <key>StandardErrorPath</key>
26
- <string>{{NEXO_HOME}}/logs/catchup-stderr.log</string>
26
+ <string>{{NEXO_HOME}}/runtime/logs/catchup-stderr.log</string>
27
27
  <key>EnvironmentVariables</key>
28
28
  <dict>
29
29
  <key>HOME</key>
@@ -14,7 +14,7 @@
14
14
  <key>ProgramArguments</key>
15
15
  <array>
16
16
  <string>/usr/bin/python3</string>
17
- <string>{{NEXO_HOME}}/scripts/nexo-cognitive-decay.py</string>
17
+ <string>{{NEXO_HOME}}/core/scripts/nexo-cognitive-decay.py</string>
18
18
  </array>
19
19
  <key>StartCalendarInterval</key>
20
20
  <dict>
@@ -24,9 +24,9 @@
24
24
  <integer>0</integer>
25
25
  </dict>
26
26
  <key>StandardOutPath</key>
27
- <string>{{NEXO_HOME}}/logs/cognitive-decay-stdout.log</string>
27
+ <string>{{NEXO_HOME}}/runtime/logs/cognitive-decay-stdout.log</string>
28
28
  <key>StandardErrorPath</key>
29
- <string>{{NEXO_HOME}}/logs/cognitive-decay-stderr.log</string>
29
+ <string>{{NEXO_HOME}}/runtime/logs/cognitive-decay-stderr.log</string>
30
30
  <key>RunAtLoad</key>
31
31
  <false/>
32
32
  <key>EnvironmentVariables</key>
@@ -15,7 +15,7 @@
15
15
  <key>ProgramArguments</key>
16
16
  <array>
17
17
  <string>/bin/bash</string>
18
- <string>{{NEXO_HOME}}/scripts/nexo-deep-sleep.sh</string>
18
+ <string>{{NEXO_HOME}}/core/scripts/nexo-deep-sleep.sh</string>
19
19
  </array>
20
20
  <key>StartCalendarInterval</key>
21
21
  <dict>
@@ -25,9 +25,9 @@
25
25
  <integer>30</integer>
26
26
  </dict>
27
27
  <key>StandardOutPath</key>
28
- <string>{{NEXO_HOME}}/logs/deep-sleep-stdout.log</string>
28
+ <string>{{NEXO_HOME}}/runtime/logs/deep-sleep-stdout.log</string>
29
29
  <key>StandardErrorPath</key>
30
- <string>{{NEXO_HOME}}/logs/deep-sleep-stderr.log</string>
30
+ <string>{{NEXO_HOME}}/runtime/logs/deep-sleep-stderr.log</string>
31
31
  <key>EnvironmentVariables</key>
32
32
  <dict>
33
33
  <key>PATH</key>
@@ -14,7 +14,7 @@
14
14
  <key>ProgramArguments</key>
15
15
  <array>
16
16
  <string>/usr/bin/python3</string>
17
- <string>{{NEXO_HOME}}/scripts/nexo-evolution-run.py</string>
17
+ <string>{{NEXO_HOME}}/core/scripts/nexo-evolution-run.py</string>
18
18
  </array>
19
19
  <key>StartCalendarInterval</key>
20
20
  <dict>
@@ -26,9 +26,9 @@
26
26
  <integer>0</integer>
27
27
  </dict>
28
28
  <key>StandardOutPath</key>
29
- <string>{{NEXO_HOME}}/logs/evolution-stdout.log</string>
29
+ <string>{{NEXO_HOME}}/runtime/logs/evolution-stdout.log</string>
30
30
  <key>StandardErrorPath</key>
31
- <string>{{NEXO_HOME}}/logs/evolution-stderr.log</string>
31
+ <string>{{NEXO_HOME}}/runtime/logs/evolution-stderr.log</string>
32
32
  <key>EnvironmentVariables</key>
33
33
  <dict>
34
34
  <key>HOME</key>
@@ -15,7 +15,7 @@
15
15
  <key>ProgramArguments</key>
16
16
  <array>
17
17
  <string>/usr/bin/python3</string>
18
- <string>{{NEXO_HOME}}/scripts/nexo-followup-hygiene.py</string>
18
+ <string>{{NEXO_HOME}}/core/scripts/nexo-followup-hygiene.py</string>
19
19
  </array>
20
20
  <key>StartCalendarInterval</key>
21
21
  <dict>
@@ -27,9 +27,9 @@
27
27
  <integer>0</integer>
28
28
  </dict>
29
29
  <key>StandardOutPath</key>
30
- <string>{{NEXO_HOME}}/logs/followup-hygiene-stdout.log</string>
30
+ <string>{{NEXO_HOME}}/runtime/logs/followup-hygiene-stdout.log</string>
31
31
  <key>StandardErrorPath</key>
32
- <string>{{NEXO_HOME}}/logs/followup-hygiene-stderr.log</string>
32
+ <string>{{NEXO_HOME}}/runtime/logs/followup-hygiene-stderr.log</string>
33
33
  <key>EnvironmentVariables</key>
34
34
  <dict>
35
35
  <key>HOME</key>
@@ -14,7 +14,7 @@
14
14
  <key>ProgramArguments</key>
15
15
  <array>
16
16
  <string>/usr/bin/python3</string>
17
- <string>{{NEXO_HOME}}/scripts/nexo-immune.py</string>
17
+ <string>{{NEXO_HOME}}/core/scripts/nexo-immune.py</string>
18
18
  </array>
19
19
 
20
20
  <key>StartInterval</key>
@@ -15,7 +15,7 @@
15
15
  <key>ProgramArguments</key>
16
16
  <array>
17
17
  <string>/usr/bin/python3</string>
18
- <string>{{NEXO_HOME}}/scripts/nexo-postmortem-consolidator.py</string>
18
+ <string>{{NEXO_HOME}}/core/scripts/nexo-postmortem-consolidator.py</string>
19
19
  </array>
20
20
  <key>RunAtLoad</key>
21
21
  <false/>
@@ -27,9 +27,9 @@
27
27
  <integer>30</integer>
28
28
  </dict>
29
29
  <key>StandardOutPath</key>
30
- <string>{{NEXO_HOME}}/logs/postmortem-stdout.log</string>
30
+ <string>{{NEXO_HOME}}/runtime/logs/postmortem-stdout.log</string>
31
31
  <key>StandardErrorPath</key>
32
- <string>{{NEXO_HOME}}/logs/postmortem-stderr.log</string>
32
+ <string>{{NEXO_HOME}}/runtime/logs/postmortem-stderr.log</string>
33
33
  <key>EnvironmentVariables</key>
34
34
  <dict>
35
35
  <key>HOME</key>
@@ -15,7 +15,7 @@
15
15
  <key>ProgramArguments</key>
16
16
  <array>
17
17
  <string>/usr/bin/python3</string>
18
- <string>{{NEXO_HOME}}/scripts/nexo-daily-self-audit.py</string>
18
+ <string>{{NEXO_HOME}}/core/scripts/nexo-daily-self-audit.py</string>
19
19
  </array>
20
20
 
21
21
  <key>StartCalendarInterval</key>
@@ -27,10 +27,10 @@
27
27
  </dict>
28
28
 
29
29
  <key>StandardOutPath</key>
30
- <string>{{NEXO_HOME}}/logs/self-audit-stdout.log</string>
30
+ <string>{{NEXO_HOME}}/runtime/logs/self-audit-stdout.log</string>
31
31
 
32
32
  <key>StandardErrorPath</key>
33
- <string>{{NEXO_HOME}}/logs/self-audit-stderr.log</string>
33
+ <string>{{NEXO_HOME}}/runtime/logs/self-audit-stderr.log</string>
34
34
 
35
35
  <key>EnvironmentVariables</key>
36
36
  <dict>
@@ -15,7 +15,7 @@
15
15
  <key>ProgramArguments</key>
16
16
  <array>
17
17
  <string>/usr/bin/python3</string>
18
- <string>{{NEXO_HOME}}/scripts/nexo-synthesis.py</string>
18
+ <string>{{NEXO_HOME}}/core/scripts/nexo-synthesis.py</string>
19
19
  </array>
20
20
 
21
21
  <key>StartCalendarInterval</key>
@@ -14,16 +14,16 @@
14
14
  <key>ProgramArguments</key>
15
15
  <array>
16
16
  <string>/bin/bash</string>
17
- <string>{{NEXO_HOME}}/scripts/nexo-watchdog.sh</string>
17
+ <string>{{NEXO_HOME}}/core/scripts/nexo-watchdog.sh</string>
18
18
  </array>
19
19
  <key>StartInterval</key>
20
20
  <integer>1800</integer>
21
21
  <key>RunAtLoad</key>
22
22
  <true/>
23
23
  <key>StandardOutPath</key>
24
- <string>{{NEXO_HOME}}/logs/watchdog-stdout.log</string>
24
+ <string>{{NEXO_HOME}}/runtime/logs/watchdog-stdout.log</string>
25
25
  <key>StandardErrorPath</key>
26
- <string>{{NEXO_HOME}}/logs/watchdog-stderr.log</string>
26
+ <string>{{NEXO_HOME}}/runtime/logs/watchdog-stderr.log</string>
27
27
  <key>EnvironmentVariables</key>
28
28
  <dict>
29
29
  <key>PATH</key>
@@ -76,7 +76,24 @@ def _resolve_core_runner(script_name: str) -> Path:
76
76
 
77
77
  def _resolve_automation_backend() -> str:
78
78
  data = _load_schedule()
79
- return str(data.get("automation_backend", "claude_code") or "claude_code")
79
+ provider_runtime = data.get("provider_runtime") if isinstance(data.get("provider_runtime"), dict) else {}
80
+ selected = str(provider_runtime.get("selected_chat_provider") or "").strip().lower()
81
+ backend_raw = str(provider_runtime.get("automation_backend") or data.get("automation_backend") or "claude_code").strip().lower()
82
+ provider_raw = str(provider_runtime.get("automation_provider") or "").strip().lower()
83
+ automation_enabled = data.get("automation_enabled", True) is not False
84
+ if (
85
+ not automation_enabled
86
+ or backend_raw in {"none", "off", "disabled", "false", "0"}
87
+ or provider_raw in {"none", "off", "disabled", "false", "0"}
88
+ ):
89
+ return "none"
90
+ provider = (
91
+ {"anthropic": "anthropic", "openai": "openai"}.get(selected)
92
+ or {"anthropic": "anthropic", "openai": "openai"}.get(provider_raw)
93
+ or {"claude_code": "anthropic", "codex": "openai"}.get(backend_raw)
94
+ or "anthropic"
95
+ )
96
+ return {"anthropic": "claude_code", "openai": "codex"}.get(provider, "claude_code")
80
97
 
81
98
 
82
99
  def _load_bootstrap_prompt() -> str: