nexo-brain 7.25.6 → 7.26.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.
@@ -45,21 +45,71 @@ from datetime import datetime, timezone
45
45
  print(datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"))
46
46
  PY
47
47
  )
48
+ RUNTIME_META=$(python3 - "$NEXO_HOME" <<'PY' 2>/dev/null || true
49
+ from __future__ import annotations
50
+ import json
51
+ import sys
52
+ from pathlib import Path
53
+
54
+ nexo_home = Path(sys.argv[1]).expanduser()
55
+ schedule = {}
56
+ for candidate in (
57
+ nexo_home / "personal" / "config" / "schedule.json",
58
+ nexo_home / "config" / "schedule.json",
59
+ ):
60
+ try:
61
+ schedule = json.loads(candidate.read_text(encoding="utf-8"))
62
+ break
63
+ except Exception:
64
+ continue
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, "")
71
+ snapshot = {
72
+ "selected_chat_provider": provider_runtime.get("selected_chat_provider") or "",
73
+ "automation_provider": provider,
74
+ "automation_backend": backend,
75
+ "fallback_policy": provider_runtime.get("fallback_policy") or {"automation": "fail_closed"},
76
+ }
77
+ print(provider + "\t" + backend + "\t" + json.dumps(snapshot, ensure_ascii=False, separators=(",", ":")))
78
+ PY
79
+ )
80
+ if [ -n "$RUNTIME_META" ]; then
81
+ IFS=$'\t' read -r CRON_PROVIDER CRON_BACKEND RUNTIME_SNAPSHOT <<< "$RUNTIME_META"
82
+ else
83
+ CRON_PROVIDER=""
84
+ CRON_BACKEND=""
85
+ RUNTIME_SNAPSHOT="{}"
86
+ fi
48
87
 
49
88
  # Phase 1: INSERT row at start (ended_at NULL = "running").
50
89
  # ROW_ID empty on DB failure; spool-fallback at the end handles that.
51
90
  ROW_ID=""
52
- ROW_ID=$(python3 - "$DB" "$CRON_ID" "$STARTED_AT" <<'PY' 2>/dev/null
91
+ ROW_ID=$(python3 - "$DB" "$CRON_ID" "$STARTED_AT" "$CRON_PROVIDER" "$CRON_BACKEND" "$RUNTIME_SNAPSHOT" <<'PY' 2>/dev/null
53
92
  from __future__ import annotations
54
93
  import sqlite3
55
94
  import sys
56
- db_path, cron_id, started_at = sys.argv[1:]
95
+ db_path, cron_id, started_at, provider, backend, runtime_snapshot = sys.argv[1:]
57
96
  conn = sqlite3.connect(db_path)
58
97
  try:
59
- cur = conn.execute(
60
- "INSERT INTO cron_runs (cron_id, started_at, ended_at) VALUES (?, ?, NULL)",
61
- (cron_id, started_at),
62
- )
98
+ try:
99
+ cur = conn.execute(
100
+ """
101
+ INSERT INTO cron_runs (cron_id, started_at, ended_at, provider, backend, runtime_snapshot)
102
+ VALUES (?, ?, NULL, ?, ?, ?)
103
+ """,
104
+ (cron_id, started_at, provider, backend, runtime_snapshot or "{}"),
105
+ )
106
+ except sqlite3.OperationalError as exc:
107
+ if "provider" not in str(exc) and "backend" not in str(exc) and "runtime_snapshot" not in str(exc):
108
+ raise
109
+ cur = conn.execute(
110
+ "INSERT INTO cron_runs (cron_id, started_at, ended_at) VALUES (?, ?, NULL)",
111
+ (cron_id, started_at),
112
+ )
63
113
  conn.commit()
64
114
  print(cur.lastrowid)
65
115
  finally:
@@ -97,30 +147,60 @@ PY
97
147
  fi
98
148
 
99
149
  # Update the row we inserted at start — or INSERT fresh if the start write failed.
100
- if ! python3 - "$DB" "$ROW_ID" "$CRON_ID" "$STARTED_AT" "$ended_at" "$EXIT_CODE" "$summary" "$error" "$duration" <<'PY' 2>/dev/null
150
+ if ! python3 - "$DB" "$ROW_ID" "$CRON_ID" "$STARTED_AT" "$ended_at" "$EXIT_CODE" "$summary" "$error" "$duration" "$CRON_PROVIDER" "$CRON_BACKEND" "$RUNTIME_SNAPSHOT" <<'PY' 2>/dev/null
101
151
  from __future__ import annotations
102
152
  import sqlite3
103
153
  import sys
104
- db_path, row_id, cron_id, started_at, ended_at, exit_code, summary, error, duration_secs = sys.argv[1:]
154
+ db_path, row_id, cron_id, started_at, ended_at, exit_code, summary, error, duration_secs, provider, backend, runtime_snapshot = sys.argv[1:]
105
155
  conn = sqlite3.connect(db_path)
106
156
  try:
107
157
  if row_id:
108
- conn.execute(
109
- """
110
- UPDATE cron_runs
111
- SET ended_at=?, exit_code=?, summary=?, error=?, duration_secs=?
112
- WHERE id=?
113
- """,
114
- (ended_at, int(exit_code), summary, error, float(duration_secs), int(row_id)),
115
- )
158
+ try:
159
+ conn.execute(
160
+ """
161
+ UPDATE cron_runs
162
+ SET ended_at=?, exit_code=?, summary=?, error=?, duration_secs=?,
163
+ provider=COALESCE(NULLIF(provider, ''), ?),
164
+ backend=COALESCE(NULLIF(backend, ''), ?),
165
+ runtime_snapshot=CASE
166
+ WHEN runtime_snapshot IS NULL OR runtime_snapshot = '' OR runtime_snapshot = '{}'
167
+ THEN ?
168
+ ELSE runtime_snapshot
169
+ END
170
+ WHERE id=?
171
+ """,
172
+ (ended_at, int(exit_code), summary, error, float(duration_secs), provider, backend, runtime_snapshot or "{}", int(row_id)),
173
+ )
174
+ except sqlite3.OperationalError as exc:
175
+ if "provider" not in str(exc) and "backend" not in str(exc) and "runtime_snapshot" not in str(exc):
176
+ raise
177
+ conn.execute(
178
+ """
179
+ UPDATE cron_runs
180
+ SET ended_at=?, exit_code=?, summary=?, error=?, duration_secs=?
181
+ WHERE id=?
182
+ """,
183
+ (ended_at, int(exit_code), summary, error, float(duration_secs), int(row_id)),
184
+ )
116
185
  else:
117
- conn.execute(
118
- """
119
- INSERT INTO cron_runs (cron_id, started_at, ended_at, exit_code, summary, error, duration_secs)
120
- VALUES (?, ?, ?, ?, ?, ?, ?)
121
- """,
122
- (cron_id, started_at, ended_at, int(exit_code), summary, error, float(duration_secs)),
123
- )
186
+ try:
187
+ conn.execute(
188
+ """
189
+ INSERT INTO cron_runs (cron_id, started_at, ended_at, exit_code, summary, error, duration_secs, provider, backend, runtime_snapshot)
190
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
191
+ """,
192
+ (cron_id, started_at, ended_at, int(exit_code), summary, error, float(duration_secs), provider, backend, runtime_snapshot or "{}"),
193
+ )
194
+ except sqlite3.OperationalError as exc:
195
+ if "provider" not in str(exc) and "backend" not in str(exc) and "runtime_snapshot" not in str(exc):
196
+ raise
197
+ conn.execute(
198
+ """
199
+ INSERT INTO cron_runs (cron_id, started_at, ended_at, exit_code, summary, error, duration_secs)
200
+ VALUES (?, ?, ?, ?, ?, ?, ?)
201
+ """,
202
+ (cron_id, started_at, ended_at, int(exit_code), summary, error, float(duration_secs)),
203
+ )
124
204
  conn.commit()
125
205
  finally:
126
206
  conn.close()
@@ -128,12 +208,12 @@ PY
128
208
  then
129
209
  mkdir -p "$SPOOL_DIR"
130
210
  local spool_file="$SPOOL_DIR/${CRON_ID}-$(date +%Y%m%d-%H%M%S)-$$.json"
131
- python3 - "$spool_file" "$CRON_ID" "$STARTED_AT" "$ended_at" "$EXIT_CODE" "$summary" "$error" "$duration" <<'PY'
211
+ python3 - "$spool_file" "$CRON_ID" "$STARTED_AT" "$ended_at" "$EXIT_CODE" "$summary" "$error" "$duration" "$CRON_PROVIDER" "$CRON_BACKEND" "$RUNTIME_SNAPSHOT" <<'PY'
132
212
  from __future__ import annotations
133
213
  import json
134
214
  import sys
135
215
  from pathlib import Path
136
- spool_file, cron_id, started_at, ended_at, exit_code, summary, error, duration_secs = sys.argv[1:]
216
+ spool_file, cron_id, started_at, ended_at, exit_code, summary, error, duration_secs, provider, backend, runtime_snapshot = sys.argv[1:]
137
217
  Path(spool_file).write_text(
138
218
  json.dumps({
139
219
  "cron_id": cron_id,
@@ -143,6 +223,9 @@ Path(spool_file).write_text(
143
223
  "summary": summary,
144
224
  "error": error,
145
225
  "duration_secs": float(duration_secs),
226
+ "provider": provider,
227
+ "backend": backend,
228
+ "runtime_snapshot": runtime_snapshot or "{}",
146
229
  }, indent=2, ensure_ascii=False) + "\n",
147
230
  encoding="utf-8",
148
231
  )
@@ -569,7 +569,7 @@ def generate_briefing(prompt: str) -> tuple[str, str]:
569
569
  output_format="json",
570
570
  append_system_prompt=render_core_prompt("morning-agent-json-output"),
571
571
  allowed_tools="Read,Glob,Grep",
572
- bare_mode=True,
572
+ bare_mode=False,
573
573
  )
574
574
  if result.returncode != 0:
575
575
  detail = (result.stderr or result.stdout or "").strip()
package/src/server.py CHANGED
@@ -440,7 +440,7 @@ def _run_kwargs_from_env() -> dict:
440
440
  # ── Session management (3 tools) ──────────────────────────────────
441
441
 
442
442
  @mcp.tool
443
- def nexo_startup(task: str = "Startup", claude_session_id: str = "", session_token: str = "", session_client: str = "", conversation_id: str = "") -> str:
443
+ def nexo_startup(task: str = "Startup", claude_session_id: str = "", session_token: str = "", session_client: str = "", session_provider: str = "", conversation_id: str = "") -> str:
444
444
  """Register new session, clean stale ones, return active sessions + alerts.
445
445
 
446
446
  Call this ONCE at the start of every conversation.
@@ -453,6 +453,7 @@ def nexo_startup(task: str = "Startup", claude_session_id: str = "", session_tok
453
453
  other clients may pass a synthetic durable token when useful.
454
454
  Pass this to enable automatic inter-terminal inbox detection when available.
455
455
  session_client: Optional client label such as `claude_code` or `codex`.
456
+ session_provider: Optional provider label such as `anthropic` or `openai`.
456
457
  conversation_id: Stable client-side conversation identifier when available.
457
458
  """
458
459
  return handle_startup(
@@ -460,6 +461,7 @@ def nexo_startup(task: str = "Startup", claude_session_id: str = "", session_tok
460
461
  claude_session_id=claude_session_id,
461
462
  session_token=session_token,
462
463
  session_client=session_client,
464
+ session_provider=session_provider,
463
465
  conversation_id=conversation_id,
464
466
  )
465
467
 
@@ -84,11 +84,12 @@ def handle_session_log_create(payload: dict | None = None, **kwargs) -> dict:
84
84
  except Exception:
85
85
  resonance_tier = ""
86
86
 
87
- from agent_runner import _record_automation_start
87
+ from agent_runner import _automation_provider_for_backend, _record_automation_start
88
88
 
89
89
  row_id, err = _record_automation_start(
90
90
  caller=caller,
91
91
  backend=backend,
92
+ provider=_automation_provider_for_backend(backend),
92
93
  session_type=session_type,
93
94
  task_profile="",
94
95
  model=model,
@@ -12,6 +12,7 @@ import threading
12
12
  from datetime import datetime, timezone
13
13
  from pathlib import Path
14
14
  from core_prompts import render_core_prompt
15
+ from client_preferences import client_to_provider, normalize_provider_key
15
16
  from db import (
16
17
  register_session, update_session, complete_session,
17
18
  get_active_sessions, clean_stale_sessions, search_sessions,
@@ -582,6 +583,7 @@ def handle_startup(
582
583
  claude_session_id: str = "",
583
584
  session_token: str = "",
584
585
  session_client: str = "",
586
+ session_provider: str = "",
585
587
  conversation_id: str = "",
586
588
  ) -> str:
587
589
  """Full startup sequence: register, clean, report.
@@ -593,12 +595,16 @@ def handle_startup(
593
595
  other clients may pass a synthetic durable ID when useful.
594
596
  Enables automatic inbox detection when hook-backed clients provide one.
595
597
  session_client: Optional client label such as `claude_code` or `codex`.
598
+ session_provider: Optional provider label such as `anthropic` or `openai`.
596
599
  """
597
600
  _set_interactive_db_timeout()
598
601
  sid = _generate_sid()
599
602
  startup_warnings: list[str] = []
600
603
  cleaned = _safe_interactive("stale-session cleanup", clean_stale_sessions, 0, startup_warnings)
601
604
  linked_session_id = (session_token or claude_session_id or "").strip()
605
+ inferred_client = (session_client or "").strip()
606
+ if not inferred_client and claude_session_id and not session_token:
607
+ inferred_client = "claude_code"
602
608
  # v6.0.7 hotfix: when the caller did not pass an explicit UUID, fall back to
603
609
  # the Claude Code SessionStart UUID written by the SessionStart hook to
604
610
  # <NEXO_HOME>/coordination/.claude-session-id. This fixes the "unknown
@@ -606,15 +612,13 @@ def handle_startup(
606
612
  # nexo_startup() without propagating the hook payload (bug revisited
607
613
  # after PR #208 — PR #208 covered the hook side; this covers the
608
614
  # startup side so every session row is born correlated).
609
- if not linked_session_id:
615
+ if not linked_session_id and (not inferred_client or inferred_client == "claude_code"):
610
616
  linked_session_id = _autodetect_claude_session_id()
611
- inferred_client = (session_client or "").strip()
612
- if not inferred_client and claude_session_id and not session_token:
613
- inferred_client = "claude_code"
614
617
  if not inferred_client and linked_session_id:
615
618
  # If we recovered the UUID from the coordination file, the only
616
619
  # client that writes there is Claude Code.
617
620
  inferred_client = "claude_code"
621
+ inferred_provider = normalize_provider_key(session_provider) or client_to_provider(inferred_client)
618
622
  conversation = str(conversation_id or "").strip()
619
623
  conflicts = []
620
624
  if conversation:
@@ -623,7 +627,7 @@ def handle_startup(
623
627
  conn = get_db()
624
628
  rows = conn.execute(
625
629
  """
626
- SELECT sid, task, last_update_epoch, external_session_id, session_client
630
+ SELECT sid, task, last_update_epoch, external_session_id, session_client, session_provider
627
631
  FROM sessions
628
632
  WHERE conversation_id = ? AND last_update_epoch > ?
629
633
  ORDER BY last_update_epoch DESC
@@ -638,9 +642,10 @@ def handle_startup(
638
642
  lambda: register_session(
639
643
  sid,
640
644
  task,
641
- claude_session_id=linked_session_id,
645
+ claude_session_id=linked_session_id if inferred_client == "claude_code" else "",
642
646
  external_session_id=linked_session_id,
643
647
  session_client=inferred_client,
648
+ session_provider=inferred_provider,
644
649
  conversation_id=conversation,
645
650
  ),
646
651
  None,
@@ -668,7 +673,7 @@ def handle_startup(
668
673
  # Backward-compatible: if the alias table does not yet exist (older
669
674
  # DB), register_claude_session_alias returns False silently and
670
675
  # the legacy sessions.claude_session_id column stays authoritative.
671
- if linked_session_id:
676
+ if linked_session_id and inferred_client == "claude_code":
672
677
  try:
673
678
  from hook_guardrails import register_claude_session_alias
674
679
  from db import get_db as _get_db
@@ -712,7 +717,7 @@ def handle_startup(
712
717
  age = _format_age(row["last_update_epoch"])
713
718
  lines.append(
714
719
  f" {row['sid']} ({age}) — {row['task']} "
715
- f"[client={row.get('session_client') or '?'} external={row.get('external_session_id') or '?'}]"
720
+ f"[provider={row.get('session_provider') or '?'} client={row.get('session_client') or '?'} external={row.get('external_session_id') or '?'}]"
716
721
  )
717
722
 
718
723
  if memory_maintenance and not memory_maintenance.get("ok"):