nexo-brain 5.0.2 → 5.0.3
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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +9 -2
- package/package.json +1 -1
- package/src/agent_runner.py +33 -2
- package/src/cli.py +2 -0
- package/src/doctor/providers/runtime.py +195 -40
- package/src/tools_drive.py +13 -10
- package/src/tools_sessions.py +15 -1
- package/templates/CLAUDE.md.template +6 -1
- package/templates/CODEX.AGENTS.md.template +6 -1
- package/templates/nexo_helper.py +2 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.3",
|
|
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
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
**NEXO Brain transforms any MCP-compatible AI agent from a stateless assistant into a cognitive partner that remembers, learns, forgets, adapts, and builds a relationship with you over time.**
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
|
-
<a href="https://
|
|
14
|
+
<a href="https://nexo-brain.com/watch/">
|
|
15
15
|
<img src="assets/nexo-brain-infographic-v5.png" alt="NEXO Brain Architecture" width="700">
|
|
16
16
|
</a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
|
-
[Watch the overview on YouTube](https://www.youtube.com/watch?v=
|
|
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
21
|
Start here:
|
|
22
22
|
- [5-minute quickstart](docs/quickstart-5-minutes.md)
|
|
@@ -87,6 +87,13 @@ Versions `3.1.7` through `3.2.0` close the recent-memory gap:
|
|
|
87
87
|
- when even that misses, NEXO now exposes raw transcript fallback tools for Claude Code and Codex session stores
|
|
88
88
|
- NEXO can now inspect itself through a live system catalog derived from canonical sources instead of relying only on stale docs or operator memory
|
|
89
89
|
|
|
90
|
+
Version `5.0.3` closes the next post-5.0 runtime gap:
|
|
91
|
+
|
|
92
|
+
- `nexo chat` now boots Claude Code and Codex with an explicit NEXO startup prompt instead of opening cold or leaking the target path as a fake prompt
|
|
93
|
+
- terminal launches now use the requested working directory as real `cwd`, so the selected project path stops behaving like chat text
|
|
94
|
+
- the vendorable `nexo_helper.py` bridge now bounds helper calls with a timeout instead of letting personal-script subprocess flows wait forever
|
|
95
|
+
- the doctor hardening from `5.0.2` remains validated on a real upgraded runtime after sync
|
|
96
|
+
|
|
90
97
|
Version `5.0.2` closes the small post-5.0.1 doctor drift:
|
|
91
98
|
|
|
92
99
|
- deep doctor now reads the live `learnings` schema correctly whether the install uses `status` or the older `archived` flag
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.3",
|
|
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",
|
package/src/agent_runner.py
CHANGED
|
@@ -22,6 +22,7 @@ from client_preferences import (
|
|
|
22
22
|
CLIENT_CODEX,
|
|
23
23
|
TERMINAL_CLIENT_KEYS,
|
|
24
24
|
load_client_preferences,
|
|
25
|
+
normalize_client_key,
|
|
25
26
|
resolve_automation_backend,
|
|
26
27
|
resolve_automation_task_profile,
|
|
27
28
|
resolve_client_runtime_profile,
|
|
@@ -37,6 +38,11 @@ MODEL_PRICING_USD_PER_1M = {
|
|
|
37
38
|
"gpt-5.4": {"input": 1.25, "cached_input": 0.125, "output": 10.0},
|
|
38
39
|
"gpt-5.4-mini": {"input": 0.25, "cached_input": 0.025, "output": 2.0},
|
|
39
40
|
}
|
|
41
|
+
INTERACTIVE_STARTUP_PROMPT = (
|
|
42
|
+
"Start as NEXO for this session now. Use the managed bootstrap already installed "
|
|
43
|
+
"for this client, run nexo_startup and nexo_heartbeat for this first turn, then "
|
|
44
|
+
"reply with one concise startup status in the user's language."
|
|
45
|
+
)
|
|
40
46
|
|
|
41
47
|
|
|
42
48
|
class AgentRunnerError(RuntimeError):
|
|
@@ -312,6 +318,26 @@ def _codex_interactive_launch_flags() -> list[str]:
|
|
|
312
318
|
return ["--sandbox", "danger-full-access", "--ask-for-approval", "never"]
|
|
313
319
|
|
|
314
320
|
|
|
321
|
+
def _interactive_startup_prompt(client: str) -> str:
|
|
322
|
+
client_key = normalize_client_key(client)
|
|
323
|
+
if client_key in {CLIENT_CLAUDE_CODE, CLIENT_CODEX}:
|
|
324
|
+
return INTERACTIVE_STARTUP_PROMPT
|
|
325
|
+
return ""
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _interactive_target_cwd(target: str | os.PathLike[str]) -> str:
|
|
329
|
+
candidate = Path(target).expanduser()
|
|
330
|
+
if candidate.exists() and candidate.is_file():
|
|
331
|
+
candidate = candidate.parent
|
|
332
|
+
try:
|
|
333
|
+
resolved = candidate.resolve()
|
|
334
|
+
except Exception:
|
|
335
|
+
resolved = candidate
|
|
336
|
+
if resolved.exists():
|
|
337
|
+
return str(resolved)
|
|
338
|
+
return str(Path.cwd())
|
|
339
|
+
|
|
340
|
+
|
|
315
341
|
def build_interactive_client_command(
|
|
316
342
|
*,
|
|
317
343
|
target: str | os.PathLike[str],
|
|
@@ -322,6 +348,7 @@ def build_interactive_client_command(
|
|
|
322
348
|
selected = resolve_terminal_client(client, preferences=prefs)
|
|
323
349
|
target_path = str(Path(target).expanduser())
|
|
324
350
|
profile = resolve_client_runtime_profile(selected, preferences=prefs)
|
|
351
|
+
startup_prompt = _interactive_startup_prompt(selected)
|
|
325
352
|
|
|
326
353
|
if selected == CLIENT_CLAUDE_CODE:
|
|
327
354
|
claude_bin = _resolve_claude_cli()
|
|
@@ -334,7 +361,9 @@ def build_interactive_client_command(
|
|
|
334
361
|
cmd.extend(["--model", profile["model"]])
|
|
335
362
|
if profile["reasoning_effort"]:
|
|
336
363
|
cmd.extend(["--effort", profile["reasoning_effort"]])
|
|
337
|
-
cmd.
|
|
364
|
+
cmd.append("--dangerously-skip-permissions")
|
|
365
|
+
if startup_prompt:
|
|
366
|
+
cmd.append(startup_prompt)
|
|
338
367
|
return selected, cmd
|
|
339
368
|
|
|
340
369
|
if selected == CLIENT_CODEX:
|
|
@@ -352,6 +381,8 @@ def build_interactive_client_command(
|
|
|
352
381
|
if profile["reasoning_effort"]:
|
|
353
382
|
cmd.extend(["-c", f'model_reasoning_effort="{profile["reasoning_effort"]}"'])
|
|
354
383
|
cmd.extend(["-C", target_path])
|
|
384
|
+
if startup_prompt:
|
|
385
|
+
cmd.append(startup_prompt)
|
|
355
386
|
return selected, cmd
|
|
356
387
|
|
|
357
388
|
raise TerminalClientUnavailableError(f"Unsupported terminal client: {selected}")
|
|
@@ -368,7 +399,7 @@ def launch_interactive_client(
|
|
|
368
399
|
launch_env = os.environ.copy()
|
|
369
400
|
if env:
|
|
370
401
|
launch_env.update(env)
|
|
371
|
-
return subprocess.run(cmd, env=launch_env)
|
|
402
|
+
return subprocess.run(cmd, env=launch_env, cwd=_interactive_target_cwd(target))
|
|
372
403
|
|
|
373
404
|
|
|
374
405
|
def build_followup_terminal_shell_command(
|
package/src/cli.py
CHANGED
|
@@ -1081,6 +1081,8 @@ def _doctor(args):
|
|
|
1081
1081
|
return 1
|
|
1082
1082
|
|
|
1083
1083
|
init_db()
|
|
1084
|
+
tier_label = getattr(args, "tier", "boot") or "boot"
|
|
1085
|
+
print(f"[NEXO] Inspecting {tier_label} diagnostics... please wait.", file=sys.stderr, flush=True)
|
|
1084
1086
|
report = run_doctor(tier=args.tier, fix=args.fix)
|
|
1085
1087
|
output = format_report(report, fmt="json" if args.json else "text")
|
|
1086
1088
|
print(output)
|
|
@@ -41,6 +41,7 @@ PROTECTED_MACOS_ROOTS = (
|
|
|
41
41
|
IMMUNE_FRESHNESS = 3600 # 1 hour (runs every 30 min)
|
|
42
42
|
WATCHDOG_FRESHNESS = 3600 # 1 hour (runs every 30 min)
|
|
43
43
|
DEFAULT_CRON_THRESHOLD = 7200 # Fallback when manifest data is unavailable
|
|
44
|
+
LIVE_PROTOCOL_SESSION_FRESHNESS = 1800 # 30 minutes
|
|
44
45
|
AUXILIARY_CORE_LAUNCHAGENT_IDS = {"dashboard", "prevent-sleep", "tcc-approve"}
|
|
45
46
|
SPECIAL_LAUNCHAGENT_IDS = {"prevent-sleep", "tcc-approve"}
|
|
46
47
|
SPECIAL_ENV_NORMALIZE_IDS = SPECIAL_LAUNCHAGENT_IDS
|
|
@@ -691,6 +692,37 @@ def _load_json(path: Path) -> dict:
|
|
|
691
692
|
return json.loads(path.read_text())
|
|
692
693
|
|
|
693
694
|
|
|
695
|
+
def _latest_cron_run_age_seconds(cron_id: str) -> float | None:
|
|
696
|
+
db_path = NEXO_HOME / "data" / "nexo.db"
|
|
697
|
+
if not db_path.is_file():
|
|
698
|
+
return None
|
|
699
|
+
try:
|
|
700
|
+
conn = sqlite3.connect(str(db_path), timeout=2)
|
|
701
|
+
try:
|
|
702
|
+
conn.row_factory = sqlite3.Row
|
|
703
|
+
table = conn.execute(
|
|
704
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='cron_runs'"
|
|
705
|
+
).fetchone()
|
|
706
|
+
if not table:
|
|
707
|
+
return None
|
|
708
|
+
row = conn.execute(
|
|
709
|
+
"SELECT MAX(started_at) AS last_run FROM cron_runs WHERE cron_id = ?",
|
|
710
|
+
(cron_id,),
|
|
711
|
+
).fetchone()
|
|
712
|
+
finally:
|
|
713
|
+
conn.close()
|
|
714
|
+
except Exception:
|
|
715
|
+
return None
|
|
716
|
+
|
|
717
|
+
last_run = row["last_run"] if row else None
|
|
718
|
+
parsed = _parse_timestamp(last_run) if last_run else None
|
|
719
|
+
if parsed is None:
|
|
720
|
+
return None
|
|
721
|
+
if parsed.tzinfo is None:
|
|
722
|
+
parsed = parsed.replace(tzinfo=dt.timezone.utc)
|
|
723
|
+
return max(0.0, time.time() - parsed.timestamp())
|
|
724
|
+
|
|
725
|
+
|
|
694
726
|
def _latest_periodic_summary(kind: str) -> dict | None:
|
|
695
727
|
pattern = f"*-{kind}-summary.json"
|
|
696
728
|
candidates: list[tuple[str, Path]] = []
|
|
@@ -1097,22 +1129,9 @@ def check_immune_status() -> DoctorCheck:
|
|
|
1097
1129
|
)
|
|
1098
1130
|
|
|
1099
1131
|
age_min = age / 60
|
|
1100
|
-
if age > IMMUNE_FRESHNESS:
|
|
1101
|
-
return DoctorCheck(
|
|
1102
|
-
id="runtime.immune_freshness",
|
|
1103
|
-
tier="runtime",
|
|
1104
|
-
status="degraded",
|
|
1105
|
-
severity="warn",
|
|
1106
|
-
summary=f"Immune status stale ({age_min:.0f} min old, threshold {IMMUNE_FRESHNESS // 60} min)",
|
|
1107
|
-
evidence=[f"{status_file} last modified {age_min:.0f} minutes ago"],
|
|
1108
|
-
repair_plan=[
|
|
1109
|
-
"Check LaunchAgent/systemd timer for immune cron",
|
|
1110
|
-
"nexo scripts call nexo_schedule_status --input '{}'",
|
|
1111
|
-
],
|
|
1112
|
-
escalation_prompt="Investigate why immune system stopped refreshing.",
|
|
1113
|
-
)
|
|
1114
1132
|
|
|
1115
|
-
# Read status for additional context
|
|
1133
|
+
# Read status for additional context even when the file is stale so the
|
|
1134
|
+
# doctor can distinguish a dead immune cron from an intentional skip window.
|
|
1116
1135
|
try:
|
|
1117
1136
|
data = _load_json(status_file)
|
|
1118
1137
|
counts = data.get("counts") or {}
|
|
@@ -1132,6 +1151,39 @@ def check_immune_status() -> DoctorCheck:
|
|
|
1132
1151
|
status = "healthy"
|
|
1133
1152
|
severity = "info"
|
|
1134
1153
|
overall = "ok"
|
|
1154
|
+
evidence: list[str] = []
|
|
1155
|
+
if age > IMMUNE_FRESHNESS:
|
|
1156
|
+
cron_age = _latest_cron_run_age_seconds("immune")
|
|
1157
|
+
if cron_age is not None and cron_age <= IMMUNE_FRESHNESS:
|
|
1158
|
+
evidence.append(
|
|
1159
|
+
f"immune cron ran {cron_age / 60:.0f} minutes ago; reusing last reported status while the cron remains alive"
|
|
1160
|
+
)
|
|
1161
|
+
summary = (
|
|
1162
|
+
f"Immune cron is alive; last status reused ({age_min:.0f} min old file)"
|
|
1163
|
+
if status == "healthy"
|
|
1164
|
+
else f"Immune cron is alive; last {overall} status reused ({age_min:.0f} min old file)"
|
|
1165
|
+
)
|
|
1166
|
+
return DoctorCheck(
|
|
1167
|
+
id="runtime.immune_freshness",
|
|
1168
|
+
tier="runtime",
|
|
1169
|
+
status=status,
|
|
1170
|
+
severity=severity,
|
|
1171
|
+
summary=summary,
|
|
1172
|
+
evidence=evidence,
|
|
1173
|
+
)
|
|
1174
|
+
return DoctorCheck(
|
|
1175
|
+
id="runtime.immune_freshness",
|
|
1176
|
+
tier="runtime",
|
|
1177
|
+
status="degraded",
|
|
1178
|
+
severity="warn",
|
|
1179
|
+
summary=f"Immune status stale ({age_min:.0f} min old, threshold {IMMUNE_FRESHNESS // 60} min)",
|
|
1180
|
+
evidence=[f"{status_file} last modified {age_min:.0f} minutes ago"],
|
|
1181
|
+
repair_plan=[
|
|
1182
|
+
"Check LaunchAgent/systemd timer for immune cron",
|
|
1183
|
+
"nexo scripts call nexo_schedule_status --input '{}'",
|
|
1184
|
+
],
|
|
1185
|
+
escalation_prompt="Investigate why immune system stopped refreshing.",
|
|
1186
|
+
)
|
|
1135
1187
|
return DoctorCheck(
|
|
1136
1188
|
id="runtime.immune_freshness",
|
|
1137
1189
|
tier="runtime",
|
|
@@ -2310,7 +2362,7 @@ def check_protocol_compliance() -> DoctorCheck:
|
|
|
2310
2362
|
tables = {
|
|
2311
2363
|
row["name"]
|
|
2312
2364
|
for row in conn.execute(
|
|
2313
|
-
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('protocol_tasks', 'protocol_debt')"
|
|
2365
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('protocol_tasks', 'protocol_debt', 'sessions')"
|
|
2314
2366
|
).fetchall()
|
|
2315
2367
|
}
|
|
2316
2368
|
tasks = None
|
|
@@ -2323,14 +2375,78 @@ def check_protocol_compliance() -> DoctorCheck:
|
|
|
2323
2375
|
ORDER BY opened_at DESC""",
|
|
2324
2376
|
(window,),
|
|
2325
2377
|
).fetchall()
|
|
2326
|
-
|
|
2327
|
-
""
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2378
|
+
protocol_debt_cols = {
|
|
2379
|
+
row["name"] for row in conn.execute("PRAGMA table_info(protocol_debt)").fetchall()
|
|
2380
|
+
}
|
|
2381
|
+
protocol_task_cols = {
|
|
2382
|
+
row["name"] for row in conn.execute("PRAGMA table_info(protocol_tasks)").fetchall()
|
|
2383
|
+
}
|
|
2384
|
+
session_cols = set()
|
|
2385
|
+
if "sessions" in tables:
|
|
2386
|
+
session_cols = {
|
|
2387
|
+
row["name"] for row in conn.execute("PRAGMA table_info(sessions)").fetchall()
|
|
2388
|
+
}
|
|
2389
|
+
active_session_ids: set[str] = set()
|
|
2390
|
+
if {"sid", "last_update_epoch"}.issubset(session_cols):
|
|
2391
|
+
active_cutoff = time.time() - LIVE_PROTOCOL_SESSION_FRESHNESS
|
|
2392
|
+
active_session_ids = {
|
|
2393
|
+
str(row["sid"] or "")
|
|
2394
|
+
for row in conn.execute(
|
|
2395
|
+
"SELECT sid FROM sessions WHERE last_update_epoch >= ?",
|
|
2396
|
+
(active_cutoff,),
|
|
2397
|
+
).fetchall()
|
|
2398
|
+
}
|
|
2399
|
+
live_guard_debt = 0
|
|
2400
|
+
if {"task_id", "session_id"}.issubset(protocol_debt_cols):
|
|
2401
|
+
task_status_expr = "'' AS task_status"
|
|
2402
|
+
if "status" in protocol_task_cols:
|
|
2403
|
+
task_status_expr = "pt.status AS task_status"
|
|
2404
|
+
open_debts = conn.execute(
|
|
2405
|
+
f"""SELECT
|
|
2406
|
+
pd.severity,
|
|
2407
|
+
pd.debt_type,
|
|
2408
|
+
pd.session_id,
|
|
2409
|
+
pd.task_id,
|
|
2410
|
+
pd.created_at,
|
|
2411
|
+
{task_status_expr}
|
|
2412
|
+
FROM protocol_debt pd
|
|
2413
|
+
LEFT JOIN protocol_tasks pt ON pt.task_id = pd.task_id
|
|
2414
|
+
WHERE pd.status = 'open' AND pd.created_at >= datetime('now', ?)
|
|
2415
|
+
ORDER BY pd.created_at DESC""",
|
|
2416
|
+
(window,),
|
|
2417
|
+
).fetchall()
|
|
2418
|
+
debt_counter: dict[tuple[str, str], int] = {}
|
|
2419
|
+
for row in open_debts:
|
|
2420
|
+
severity = str(row["severity"] or "warn")
|
|
2421
|
+
debt_type = str(row["debt_type"] or "unknown")
|
|
2422
|
+
session_id = str(row["session_id"] or "")
|
|
2423
|
+
task_status = str(row["task_status"] or "")
|
|
2424
|
+
if (
|
|
2425
|
+
debt_type == "unacknowledged_guard_blocking"
|
|
2426
|
+
and task_status == "open"
|
|
2427
|
+
and session_id in active_session_ids
|
|
2428
|
+
):
|
|
2429
|
+
live_guard_debt += 1
|
|
2430
|
+
continue
|
|
2431
|
+
debt_counter[(severity, debt_type)] = debt_counter.get((severity, debt_type), 0) + 1
|
|
2432
|
+
debt_rows = [
|
|
2433
|
+
{"severity": severity, "debt_type": debt_type, "total": total}
|
|
2434
|
+
for (severity, debt_type), total in sorted(
|
|
2435
|
+
debt_counter.items(),
|
|
2436
|
+
key=lambda item: (-item[1], item[0][1], item[0][0]),
|
|
2437
|
+
)
|
|
2438
|
+
]
|
|
2439
|
+
else:
|
|
2440
|
+
debt_rows = [
|
|
2441
|
+
dict(row) for row in conn.execute(
|
|
2442
|
+
"""SELECT severity, debt_type, COUNT(*) AS total
|
|
2443
|
+
FROM protocol_debt
|
|
2444
|
+
WHERE status = 'open' AND created_at >= datetime('now', ?)
|
|
2445
|
+
GROUP BY severity, debt_type
|
|
2446
|
+
ORDER BY total DESC, debt_type ASC""",
|
|
2447
|
+
(window,),
|
|
2448
|
+
).fetchall()
|
|
2449
|
+
]
|
|
2334
2450
|
has_cortex_evaluations = bool(
|
|
2335
2451
|
conn.execute(
|
|
2336
2452
|
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='cortex_evaluations'"
|
|
@@ -2417,6 +2533,8 @@ def check_protocol_compliance() -> DoctorCheck:
|
|
|
2417
2533
|
evidence.append("high-stakes decision-eval rollout not yet seeded in the live window")
|
|
2418
2534
|
for row in debt_rows[:5]:
|
|
2419
2535
|
evidence.append(f"open {row['severity']} debt — {row['debt_type']}: {row['total']}")
|
|
2536
|
+
if live_guard_debt:
|
|
2537
|
+
evidence.append(f"live in-progress guard acknowledgements pending: {live_guard_debt}")
|
|
2420
2538
|
|
|
2421
2539
|
repair_plan: list[str] = []
|
|
2422
2540
|
if verify_required and len(verify_ok) != len(verify_required):
|
|
@@ -2725,20 +2843,51 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
2725
2843
|
repair_plan=["Run NEXO migrations before trusting automation cost/parity metrics"],
|
|
2726
2844
|
escalation_prompt="Shared automation runs are happening without the telemetry table that release metrics depend on.",
|
|
2727
2845
|
)
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
""
|
|
2740
|
-
|
|
2741
|
-
|
|
2846
|
+
columns = {
|
|
2847
|
+
row["name"] for row in conn.execute("PRAGMA table_info(automation_runs)").fetchall()
|
|
2848
|
+
}
|
|
2849
|
+
schema_has_status = {
|
|
2850
|
+
"status",
|
|
2851
|
+
"input_tokens",
|
|
2852
|
+
"cached_input_tokens",
|
|
2853
|
+
"output_tokens",
|
|
2854
|
+
"total_cost_usd",
|
|
2855
|
+
"cost_source",
|
|
2856
|
+
"backend",
|
|
2857
|
+
"created_at",
|
|
2858
|
+
}.issubset(columns)
|
|
2859
|
+
if schema_has_status:
|
|
2860
|
+
row = conn.execute(
|
|
2861
|
+
"""
|
|
2862
|
+
SELECT
|
|
2863
|
+
COUNT(*) AS runs,
|
|
2864
|
+
SUM(CASE WHEN status = 'ok' THEN 1 ELSE 0 END) AS successful_runs,
|
|
2865
|
+
SUM(CASE WHEN status != 'ok' THEN 1 ELSE 0 END) AS failed_runs,
|
|
2866
|
+
SUM(CASE WHEN status = 'ok' AND (input_tokens + cached_input_tokens + output_tokens) > 0 THEN 1 ELSE 0 END) AS usage_runs,
|
|
2867
|
+
SUM(CASE WHEN status = 'ok' AND total_cost_usd IS NOT NULL THEN 1 ELSE 0 END) AS cost_runs,
|
|
2868
|
+
SUM(CASE WHEN status = 'ok' AND cost_source = 'pricing_unavailable' THEN 1 ELSE 0 END) AS pricing_gaps,
|
|
2869
|
+
GROUP_CONCAT(DISTINCT backend) AS backends
|
|
2870
|
+
FROM automation_runs
|
|
2871
|
+
WHERE created_at >= datetime('now', ?)
|
|
2872
|
+
""",
|
|
2873
|
+
(f"-{days} days",),
|
|
2874
|
+
).fetchone()
|
|
2875
|
+
else:
|
|
2876
|
+
row = conn.execute(
|
|
2877
|
+
"""
|
|
2878
|
+
SELECT
|
|
2879
|
+
COUNT(*) AS runs,
|
|
2880
|
+
COUNT(*) AS successful_runs,
|
|
2881
|
+
0 AS failed_runs,
|
|
2882
|
+
SUM(CASE WHEN (input_tokens + cached_input_tokens + output_tokens) > 0 THEN 1 ELSE 0 END) AS usage_runs,
|
|
2883
|
+
SUM(CASE WHEN total_cost_usd IS NOT NULL THEN 1 ELSE 0 END) AS cost_runs,
|
|
2884
|
+
SUM(CASE WHEN cost_source = 'pricing_unavailable' THEN 1 ELSE 0 END) AS pricing_gaps,
|
|
2885
|
+
GROUP_CONCAT(DISTINCT backend) AS backends
|
|
2886
|
+
FROM automation_runs
|
|
2887
|
+
WHERE created_at >= datetime('now', ?)
|
|
2888
|
+
""",
|
|
2889
|
+
(f"-{days} days",),
|
|
2890
|
+
).fetchone()
|
|
2742
2891
|
finally:
|
|
2743
2892
|
conn.close()
|
|
2744
2893
|
except Exception as exc:
|
|
@@ -2766,14 +2915,20 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
2766
2915
|
escalation_prompt="",
|
|
2767
2916
|
)
|
|
2768
2917
|
|
|
2918
|
+
successful_runs = int((row["successful_runs"] if row else 0) or 0)
|
|
2919
|
+
failed_runs = int((row["failed_runs"] if row else 0) or 0)
|
|
2769
2920
|
usage_runs = int((row["usage_runs"] if row else 0) or 0)
|
|
2770
2921
|
cost_runs = int((row["cost_runs"] if row else 0) or 0)
|
|
2771
2922
|
pricing_gaps = int((row["pricing_gaps"] if row else 0) or 0)
|
|
2772
|
-
|
|
2773
|
-
|
|
2923
|
+
usage_denominator = successful_runs or total_runs
|
|
2924
|
+
cost_denominator = successful_runs or total_runs
|
|
2925
|
+
usage_coverage = round((usage_runs / usage_denominator) * 100, 1) if usage_denominator else 100.0
|
|
2926
|
+
cost_coverage = round((cost_runs / cost_denominator) * 100, 1) if cost_denominator else 100.0
|
|
2774
2927
|
evidence = [
|
|
2775
2928
|
f"window={days}d",
|
|
2776
2929
|
f"runs={total_runs}",
|
|
2930
|
+
f"successful_runs={successful_runs}",
|
|
2931
|
+
f"failed_runs={failed_runs}",
|
|
2777
2932
|
f"usage_coverage={usage_coverage}%",
|
|
2778
2933
|
f"cost_coverage={cost_coverage}%",
|
|
2779
2934
|
f"pricing_gaps={pricing_gaps}",
|
package/src/tools_drive.py
CHANGED
|
@@ -330,16 +330,17 @@ def _regex_fallback_classify(text: str) -> str | None:
|
|
|
330
330
|
return None
|
|
331
331
|
|
|
332
332
|
|
|
333
|
-
def _classify_signal(text: str) -> str | None:
|
|
333
|
+
def _classify_signal(text: str, *, allow_llm: bool = True) -> str | None:
|
|
334
334
|
"""Classify text into a signal type, or None if nothing interesting."""
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
335
|
+
if allow_llm:
|
|
336
|
+
llm_result = _llm_classify_signal(text)
|
|
337
|
+
if llm_result.get("available"):
|
|
338
|
+
confidence = float(llm_result.get("confidence", 0.0) or 0.0)
|
|
339
|
+
label = llm_result.get("label")
|
|
340
|
+
if label is None and confidence >= _LLM_CONFIDENCE_THRESHOLD:
|
|
341
|
+
return None
|
|
342
|
+
if isinstance(label, str) and confidence >= _LLM_CONFIDENCE_THRESHOLD:
|
|
343
|
+
return label
|
|
343
344
|
|
|
344
345
|
scores = _semantic_signal_scores(text)
|
|
345
346
|
if scores:
|
|
@@ -378,6 +379,8 @@ def detect_drive_signal(
|
|
|
378
379
|
source: str,
|
|
379
380
|
source_id: str = "",
|
|
380
381
|
area: str = "",
|
|
382
|
+
*,
|
|
383
|
+
allow_llm: bool = False,
|
|
381
384
|
) -> dict | None:
|
|
382
385
|
"""Analyze text for interesting signals. Creates or reinforces.
|
|
383
386
|
|
|
@@ -387,7 +390,7 @@ def detect_drive_signal(
|
|
|
387
390
|
if not context_hint or len(context_hint.strip()) < 15:
|
|
388
391
|
return None
|
|
389
392
|
|
|
390
|
-
signal_type = _classify_signal(context_hint)
|
|
393
|
+
signal_type = _classify_signal(context_hint, allow_llm=allow_llm)
|
|
391
394
|
if not signal_type:
|
|
392
395
|
return None
|
|
393
396
|
|
package/src/tools_sessions.py
CHANGED
|
@@ -31,6 +31,14 @@ SESSION_PORTABILITY_DIR = NEXO_HOME / "operations" / "session-portability"
|
|
|
31
31
|
_keepalive_threads: dict[str, threading.Event] = {} # sid → stop_event
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def _env_flag(name: str, default: bool = False) -> bool:
|
|
35
|
+
"""Parse a boolean environment flag with sane falsey values."""
|
|
36
|
+
raw = os.environ.get(name)
|
|
37
|
+
if raw is None:
|
|
38
|
+
return default
|
|
39
|
+
return raw.strip().lower() not in {"", "0", "false", "no", "off"}
|
|
40
|
+
|
|
41
|
+
|
|
34
42
|
def _keepalive_loop(sid: str, stop_event: threading.Event) -> None:
|
|
35
43
|
"""Periodically touch the session's last_update_epoch until stopped."""
|
|
36
44
|
while not stop_event.wait(KEEPALIVE_INTERVAL):
|
|
@@ -522,7 +530,13 @@ def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
|
|
|
522
530
|
try:
|
|
523
531
|
if context_hint and len(context_hint.strip()) >= 15:
|
|
524
532
|
from tools_drive import detect_drive_signal as _detect_drive
|
|
525
|
-
|
|
533
|
+
_drive_allow_llm = _env_flag("NEXO_DRIVE_LLM_IN_HEARTBEAT", default=False)
|
|
534
|
+
_drive_result = _detect_drive(
|
|
535
|
+
context_hint,
|
|
536
|
+
source="heartbeat",
|
|
537
|
+
source_id=sid,
|
|
538
|
+
allow_llm=_drive_allow_llm,
|
|
539
|
+
)
|
|
526
540
|
if _drive_result:
|
|
527
541
|
# Check for READY signals relevant to current area
|
|
528
542
|
from db import get_drive_signals as _get_drive
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- nexo-claude-md-version: 2.1.
|
|
1
|
+
<!-- nexo-claude-md-version: 2.1.3 -->
|
|
2
2
|
******CORE******
|
|
3
3
|
<!-- nexo:core:start -->
|
|
4
4
|
# {{NAME}} — Cognitive Co-Operator
|
|
@@ -28,6 +28,11 @@ NEXO updates may rewrite `CORE`, but they must preserve `USER` verbatim.
|
|
|
28
28
|
5. If a correction revealed a reusable pattern, capture or supersede the learning immediately so contradictory rules do not remain active.
|
|
29
29
|
6. On clear end-of-session intent, write the diary before replying.
|
|
30
30
|
|
|
31
|
+
## Response Pacing
|
|
32
|
+
- After the first relevant tool or artifact result, reply with the answer immediately instead of silently chaining more investigation.
|
|
33
|
+
- Only continue into deeper investigation before the first visible answer if the user explicitly asked for a deep investigation or the situation is urgent/high-risk.
|
|
34
|
+
- For single-artifact asks (email, message, diary item, reminder, prior fact), retrieve the artifact, summarize it, then decide whether further action is needed.
|
|
35
|
+
|
|
31
36
|
<!-- nexo:start:profile -->
|
|
32
37
|
## User Profile
|
|
33
38
|
- **Calibration:** `{{NEXO_HOME}}/brain/calibration.json` (personality settings + language + user name)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- nexo-codex-agents-version: 1.2.
|
|
1
|
+
<!-- nexo-codex-agents-version: 1.2.3 -->
|
|
2
2
|
******CORE******
|
|
3
3
|
<!-- nexo:core:start -->
|
|
4
4
|
# {{NAME}} — NEXO Shared Brain for Codex
|
|
@@ -25,6 +25,11 @@ NEXO updates may rewrite `CORE`, but they must preserve `USER` verbatim.
|
|
|
25
25
|
5. If a correction revealed a reusable pattern, capture or supersede the learning immediately so contradictory rules do not remain active.
|
|
26
26
|
6. On clear end-of-session intent, write the diary before replying.
|
|
27
27
|
|
|
28
|
+
## Response Pacing
|
|
29
|
+
- After the first relevant tool or artifact result, answer immediately instead of silently chaining more investigation.
|
|
30
|
+
- Only keep investigating before the first visible answer if the user explicitly requested deep investigation or the situation is urgent/high-risk.
|
|
31
|
+
- For single-artifact asks (email, message, diary item, reminder, prior fact), retrieve it, summarize it, then decide if more work is needed.
|
|
32
|
+
|
|
28
33
|
## Codex Runtime Notes
|
|
29
34
|
- Codex does not provide Claude Code hooks, so protocol discipline must be explicit.
|
|
30
35
|
- If a stable session token is useful, pass `session_token='codex-<task>-<date>'` and `session_client='codex'` to `nexo_startup`; otherwise leave them blank.
|
package/templates/nexo_helper.py
CHANGED
|
@@ -17,6 +17,7 @@ from pathlib import Path
|
|
|
17
17
|
|
|
18
18
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
19
19
|
DEFAULT_ALLOWED_TOOLS = "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"
|
|
20
|
+
DEFAULT_NEXO_TIMEOUT_SECONDS = max(15, int(os.environ.get("NEXO_HELPER_TIMEOUT", "90")))
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _load_schedule() -> dict:
|
|
@@ -57,6 +58,7 @@ def run_nexo(args: list[str]) -> str:
|
|
|
57
58
|
["nexo", *args],
|
|
58
59
|
capture_output=True,
|
|
59
60
|
text=True,
|
|
61
|
+
timeout=DEFAULT_NEXO_TIMEOUT_SECONDS,
|
|
60
62
|
)
|
|
61
63
|
if result.returncode != 0:
|
|
62
64
|
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"nexo exited {result.returncode}")
|