nexo-brain 6.0.3 → 6.0.5
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 +1 -1
- package/bin/nexo-brain.js +46 -3
- package/package.json +1 -1
- package/src/agent_runner.py +88 -19
- package/src/auto_update.py +12 -1
- package/src/client_sync.py +23 -1
- package/src/hook_guardrails.py +36 -1
- package/src/plugin_loader.py +11 -1
- package/src/plugins/update.py +12 -1
- package/src/resonance_map.py +4 -0
- package/src/script_registry.py +4 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.5",
|
|
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,7 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `6.0.
|
|
21
|
+
Version `6.0.5` is the current packaged-runtime line: the strict pre-tool guardrail no longer blocks `Edit`/`Write` with *"unknown target"* when Claude Code's `PreToolUse` payload omits `session_id`. `process_pre_tool_event` now falls back to `$NEXO_HOME/coordination/.claude-session-id` (written by the SessionStart hook) before giving up, so a user who already called `nexo_startup` + `nexo_task_open` + `nexo_guard_check` + `nexo_track` stops seeing the block storm that several Claude Code versions triggered in 6.0.2–6.0.4. Fail-closed semantics are preserved: if neither the payload nor the coordination file yields a session id the guardrail still blocks with `missing_startup`. Release also lands a new `.github/workflows/tests.yml` that runs the full `pytest` suite on every PR — the hook-guardrail regressions that shipped unnoticed in 6.0.2+ would have been caught by CI had this job existed then.
|
|
22
22
|
|
|
23
23
|
Previously in `6.0.2`: adds the reserved caller prefix `personal/*` so scripts living in `~/.nexo/scripts/` can invoke the automation backend with their own caller id without editing `src/resonance_map.py`. New kwarg `tier` (`"maximo"` / `"alto"` / `"medio"` / `"bajo"`) on `run_automation_prompt`, `run_automation_interactive`, `nexo_helper.run_automation_text`, `nexo_helper.run_automation_json`, and `nexo-agent-run.py --tier`. Precedence for `personal/*` callers: explicit `tier=` → explicit `reasoning_effort=` → `calibration.preferences.default_resonance` → `DEFAULT_RESONANCE` (`alto`). Registered callers keep their behaviour unchanged. New guide: [`docs/personal-scripts-guide.md`](docs/personal-scripts-guide.md).
|
|
24
24
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -93,10 +93,50 @@ const RESONANCE_TIER_NAMES = ["maximo", "alto", "medio", "bajo"];
|
|
|
93
93
|
const DEFAULT_RESONANCE_TIER = _RESONANCE_TIERS.default_tier || "alto";
|
|
94
94
|
|
|
95
95
|
function isEphemeralInstall(nexoHome) {
|
|
96
|
-
const
|
|
96
|
+
const os = require("os");
|
|
97
|
+
const homeDir = os.homedir();
|
|
97
98
|
const allowEphemeral = process.env.NEXO_ALLOW_EPHEMERAL_INSTALL === "1";
|
|
98
99
|
if (allowEphemeral) return false;
|
|
99
|
-
|
|
100
|
+
|
|
101
|
+
const normalize = (candidate) => {
|
|
102
|
+
if (!candidate) return "";
|
|
103
|
+
let resolved = String(candidate);
|
|
104
|
+
try {
|
|
105
|
+
resolved = fs.realpathSync.native(resolved);
|
|
106
|
+
} catch {
|
|
107
|
+
try {
|
|
108
|
+
resolved = fs.realpathSync(resolved);
|
|
109
|
+
} catch {
|
|
110
|
+
resolved = path.resolve(resolved);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return resolved.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const tempRoots = new Set();
|
|
117
|
+
for (const root of [os.tmpdir(), "/tmp", "/var/folders", "/private/var/folders"]) {
|
|
118
|
+
const normalized = normalize(root);
|
|
119
|
+
if (!normalized) continue;
|
|
120
|
+
tempRoots.add(normalized);
|
|
121
|
+
if (normalized === "/tmp") {
|
|
122
|
+
tempRoots.add("/private/tmp");
|
|
123
|
+
} else if (normalized === "/private/tmp") {
|
|
124
|
+
tempRoots.add("/tmp");
|
|
125
|
+
} else if (normalized.startsWith("/var/")) {
|
|
126
|
+
tempRoots.add(`/private${normalized}`);
|
|
127
|
+
} else if (normalized.startsWith("/private/var/")) {
|
|
128
|
+
tempRoots.add(normalized.replace(/^\/private/, ""));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const isWithin = (candidate, root) => (
|
|
133
|
+
candidate === root || candidate.startsWith(`${root}/`)
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return [nexoHome, homeDir]
|
|
137
|
+
.map(normalize)
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.some((candidate) => Array.from(tempRoots).some((root) => isWithin(candidate, root)));
|
|
100
140
|
}
|
|
101
141
|
|
|
102
142
|
const rl = readline.createInterface({
|
|
@@ -3325,7 +3365,10 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
|
|
|
3325
3365
|
schedule = await maybeConfigurePublicContribution(schedule, useDefaults);
|
|
3326
3366
|
schedule = await maybeConfigureFullDiskAccess(schedule, useDefaults, python);
|
|
3327
3367
|
const enabledOptionals = { dashboard: doDashboard, automation: schedule.automation_enabled !== false };
|
|
3328
|
-
|
|
3368
|
+
const smokeTestMode = process.env.NEXO_TESTING_SMOKE === "1";
|
|
3369
|
+
if (smokeTestMode) {
|
|
3370
|
+
log("Smoke test mode detected — skipping LaunchAgents installation.");
|
|
3371
|
+
} else if (isEphemeralInstall(NEXO_HOME)) {
|
|
3329
3372
|
log("Ephemeral HOME/NEXO_HOME detected — skipping LaunchAgents installation.");
|
|
3330
3373
|
} else {
|
|
3331
3374
|
installAllProcesses(platform, python, NEXO_HOME, schedule, LAUNCH_AGENTS, enabledOptionals);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.5",
|
|
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",
|
package/src/agent_runner.py
CHANGED
|
@@ -457,16 +457,75 @@ def _interactive_target_cwd(target: str | os.PathLike[str]) -> str:
|
|
|
457
457
|
return str(Path.cwd())
|
|
458
458
|
|
|
459
459
|
|
|
460
|
+
def _resolve_interactive_model_and_effort(
|
|
461
|
+
caller: str,
|
|
462
|
+
backend: str,
|
|
463
|
+
*,
|
|
464
|
+
preferences: dict | None = None,
|
|
465
|
+
tier: str | None = None,
|
|
466
|
+
) -> tuple[str, str]:
|
|
467
|
+
"""Return ``(model, effort)`` for an interactive launch.
|
|
468
|
+
|
|
469
|
+
v6.0.4 — interactive launchers (nexo chat, dashboard followup) were
|
|
470
|
+
picking the command flags straight from ``client_runtime_profiles``
|
|
471
|
+
(config/schedule.json), ignoring the user's ``default_resonance``
|
|
472
|
+
preference in calibration.json. That meant Preferences → "Alto"
|
|
473
|
+
never reached ``nexo chat`` even though the same preference was
|
|
474
|
+
applied correctly in headless runs (run_automation_prompt) and in
|
|
475
|
+
NEXO Desktop (lib/claude-runtime.js).
|
|
476
|
+
|
|
477
|
+
Resolution order (mirrors run_automation_prompt):
|
|
478
|
+
1. resonance_map → (model, effort) for the registered caller,
|
|
479
|
+
honouring user_default and the explicit tier override.
|
|
480
|
+
2. If the resonance_map is unavailable or returns blanks, fall
|
|
481
|
+
back to ``client_runtime_profiles`` so we stay backward
|
|
482
|
+
compatible with pre-6.0.0 installs missing resonance_tiers.json.
|
|
483
|
+
"""
|
|
484
|
+
profile = resolve_client_runtime_profile(backend, preferences=preferences)
|
|
485
|
+
model = ""
|
|
486
|
+
effort = ""
|
|
487
|
+
try:
|
|
488
|
+
from resonance_map import resolve_model_and_effort
|
|
489
|
+
|
|
490
|
+
user_default = ""
|
|
491
|
+
if isinstance(preferences, dict):
|
|
492
|
+
user_default = str(preferences.get("default_resonance") or "").strip()
|
|
493
|
+
explicit_tier = (tier or "").strip() or None
|
|
494
|
+
mapped_model, mapped_effort = resolve_model_and_effort(
|
|
495
|
+
caller,
|
|
496
|
+
backend,
|
|
497
|
+
user_default=user_default or None,
|
|
498
|
+
explicit_tier=explicit_tier,
|
|
499
|
+
)
|
|
500
|
+
if mapped_model:
|
|
501
|
+
model = mapped_model
|
|
502
|
+
if mapped_effort:
|
|
503
|
+
effort = mapped_effort
|
|
504
|
+
except Exception:
|
|
505
|
+
# resonance_map missing or caller not registered — fall back to the
|
|
506
|
+
# legacy client_runtime_profiles path so nothing explodes.
|
|
507
|
+
pass
|
|
508
|
+
if not model:
|
|
509
|
+
model = profile.get("model", "")
|
|
510
|
+
if not effort:
|
|
511
|
+
effort = profile.get("reasoning_effort", "")
|
|
512
|
+
return model, effort
|
|
513
|
+
|
|
514
|
+
|
|
460
515
|
def build_interactive_client_command(
|
|
461
516
|
*,
|
|
462
517
|
target: str | os.PathLike[str],
|
|
463
518
|
client: str | None = None,
|
|
464
519
|
preferences: dict | None = None,
|
|
520
|
+
caller: str = "nexo_chat",
|
|
521
|
+
tier: str | None = None,
|
|
465
522
|
) -> tuple[str, list[str]]:
|
|
466
523
|
prefs = preferences or load_client_preferences()
|
|
467
524
|
selected = resolve_terminal_client(client, preferences=prefs)
|
|
468
525
|
target_path = str(Path(target).expanduser())
|
|
469
|
-
|
|
526
|
+
resolved_model, resolved_effort = _resolve_interactive_model_and_effort(
|
|
527
|
+
caller, selected, preferences=prefs, tier=tier
|
|
528
|
+
)
|
|
470
529
|
startup_prompt = _interactive_startup_prompt(selected)
|
|
471
530
|
|
|
472
531
|
if selected == CLIENT_CLAUDE_CODE:
|
|
@@ -476,10 +535,10 @@ def build_interactive_client_command(
|
|
|
476
535
|
"Claude Code launcher not found in PATH. Install `claude` first."
|
|
477
536
|
)
|
|
478
537
|
cmd = [claude_bin]
|
|
479
|
-
if
|
|
480
|
-
cmd.extend(["--model",
|
|
481
|
-
if
|
|
482
|
-
cmd.extend(["--effort",
|
|
538
|
+
if resolved_model:
|
|
539
|
+
cmd.extend(["--model", resolved_model])
|
|
540
|
+
if resolved_effort:
|
|
541
|
+
cmd.extend(["--effort", resolved_effort])
|
|
483
542
|
cmd.append("--dangerously-skip-permissions")
|
|
484
543
|
if startup_prompt:
|
|
485
544
|
cmd.append(startup_prompt)
|
|
@@ -495,10 +554,10 @@ def build_interactive_client_command(
|
|
|
495
554
|
bootstrap_prompt = _load_client_bootstrap_prompt(CLIENT_CODEX)
|
|
496
555
|
if bootstrap_prompt and not _codex_managed_initial_messages_enabled():
|
|
497
556
|
cmd.extend(["-c", _codex_initial_messages_config(bootstrap_prompt)])
|
|
498
|
-
if
|
|
499
|
-
cmd.extend(["-m",
|
|
500
|
-
if
|
|
501
|
-
cmd.extend(["-c", f'model_reasoning_effort="{
|
|
557
|
+
if resolved_model:
|
|
558
|
+
cmd.extend(["-m", resolved_model])
|
|
559
|
+
if resolved_effort:
|
|
560
|
+
cmd.extend(["-c", f'model_reasoning_effort="{resolved_effort}"'])
|
|
502
561
|
cmd.extend(["-C", target_path])
|
|
503
562
|
if startup_prompt:
|
|
504
563
|
cmd.append(startup_prompt)
|
|
@@ -548,8 +607,14 @@ def run_automation_interactive(
|
|
|
548
607
|
the interactive surface honours whatever the user selected.
|
|
549
608
|
"""
|
|
550
609
|
prefs = preferences or load_client_preferences()
|
|
610
|
+
# v6.0.4 — caller+tier propagate into the builder so interactive launches
|
|
611
|
+
# honour default_resonance (previously ignored for nexo chat).
|
|
551
612
|
resolved_client, cmd = build_interactive_client_command(
|
|
552
|
-
target=target,
|
|
613
|
+
target=target,
|
|
614
|
+
client=client,
|
|
615
|
+
preferences=prefs,
|
|
616
|
+
caller=caller,
|
|
617
|
+
tier=(tier or "").strip() or None,
|
|
553
618
|
)
|
|
554
619
|
launch_env = os.environ.copy()
|
|
555
620
|
if env:
|
|
@@ -616,10 +681,14 @@ def build_followup_terminal_shell_command(
|
|
|
616
681
|
client: str | None = None,
|
|
617
682
|
preferences: dict | None = None,
|
|
618
683
|
cwd: str | os.PathLike[str] | None = None,
|
|
684
|
+
caller: str = "nexo_followup_terminal",
|
|
685
|
+
tier: str | None = None,
|
|
619
686
|
) -> tuple[str, str]:
|
|
620
687
|
prefs = preferences or load_client_preferences()
|
|
621
688
|
selected = resolve_terminal_client(client, preferences=prefs)
|
|
622
|
-
|
|
689
|
+
resolved_model, resolved_effort = _resolve_interactive_model_and_effort(
|
|
690
|
+
caller, selected, preferences=prefs, tier=tier
|
|
691
|
+
)
|
|
623
692
|
prompt = f"NEXO: execute followup from file $(cat {followup_reference})"
|
|
624
693
|
|
|
625
694
|
if selected == CLIENT_CLAUDE_CODE:
|
|
@@ -629,10 +698,10 @@ def build_followup_terminal_shell_command(
|
|
|
629
698
|
"Claude Code launcher not found in PATH. Install `claude` first."
|
|
630
699
|
)
|
|
631
700
|
cmd = [claude_bin]
|
|
632
|
-
if
|
|
633
|
-
cmd.extend(["--model",
|
|
634
|
-
if
|
|
635
|
-
cmd.extend(["--effort",
|
|
701
|
+
if resolved_model:
|
|
702
|
+
cmd.extend(["--model", resolved_model])
|
|
703
|
+
if resolved_effort:
|
|
704
|
+
cmd.extend(["--effort", resolved_effort])
|
|
636
705
|
cmd.extend(["--dangerously-skip-permissions", prompt])
|
|
637
706
|
return selected, shlex.join(cmd)
|
|
638
707
|
|
|
@@ -647,10 +716,10 @@ def build_followup_terminal_shell_command(
|
|
|
647
716
|
bootstrap_prompt = _load_client_bootstrap_prompt(CLIENT_CODEX)
|
|
648
717
|
if bootstrap_prompt and not _codex_managed_initial_messages_enabled():
|
|
649
718
|
cmd.extend(["-c", _codex_initial_messages_config(bootstrap_prompt)])
|
|
650
|
-
if
|
|
651
|
-
cmd.extend(["-m",
|
|
652
|
-
if
|
|
653
|
-
cmd.extend(["-c", f'model_reasoning_effort="{
|
|
719
|
+
if resolved_model:
|
|
720
|
+
cmd.extend(["-m", resolved_model])
|
|
721
|
+
if resolved_effort:
|
|
722
|
+
cmd.extend(["-c", f'model_reasoning_effort="{resolved_effort}"'])
|
|
654
723
|
cmd.extend(["-C", target_cwd, prompt])
|
|
655
724
|
return selected, shlex.join(cmd)
|
|
656
725
|
|
package/src/auto_update.py
CHANGED
|
@@ -18,7 +18,18 @@ import time
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
20
|
from runtime_home import export_resolved_nexo_home, managed_nexo_home
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from tree_hygiene import is_duplicate_artifact_name
|
|
24
|
+
except ModuleNotFoundError as exc:
|
|
25
|
+
if getattr(exc, "name", "") != "tree_hygiene":
|
|
26
|
+
raise
|
|
27
|
+
|
|
28
|
+
# Older installed runtimes may update into code that references
|
|
29
|
+
# tree_hygiene.py before that module has been copied over. Fall back
|
|
30
|
+
# to "no duplicate" so the update can complete and deliver the module.
|
|
31
|
+
def is_duplicate_artifact_name(_path) -> bool:
|
|
32
|
+
return False
|
|
22
33
|
|
|
23
34
|
NEXO_HOME = export_resolved_nexo_home()
|
|
24
35
|
DATA_DIR = NEXO_HOME / "data"
|
package/src/client_sync.py
CHANGED
|
@@ -542,9 +542,31 @@ CORE_HOOK_SPECS = [
|
|
|
542
542
|
},
|
|
543
543
|
]
|
|
544
544
|
|
|
545
|
+
# Claude Code can retain legacy managed hooks from older/plugin-style installs
|
|
546
|
+
# or from transient test runtimes. Treat their handler basenames as managed so
|
|
547
|
+
# the next sync prunes them instead of leaving broken duplicates behind.
|
|
545
548
|
LEGACY_CORE_HOOK_IDENTITIES_BY_EVENT = {
|
|
549
|
+
"SessionStart": {
|
|
550
|
+
"session_start.py",
|
|
551
|
+
},
|
|
552
|
+
"Stop": {
|
|
553
|
+
"stop.py",
|
|
554
|
+
},
|
|
555
|
+
"UserPromptSubmit": {
|
|
556
|
+
"auto_capture.py",
|
|
557
|
+
},
|
|
546
558
|
"PostToolUse": {
|
|
547
559
|
"heartbeat-guard.sh",
|
|
560
|
+
"post_tool_use.py",
|
|
561
|
+
},
|
|
562
|
+
"PreCompact": {
|
|
563
|
+
"pre_compact.py",
|
|
564
|
+
},
|
|
565
|
+
"Notification": {
|
|
566
|
+
"notification.py",
|
|
567
|
+
},
|
|
568
|
+
"SubagentStop": {
|
|
569
|
+
"subagent_stop.py",
|
|
548
570
|
},
|
|
549
571
|
}
|
|
550
572
|
|
|
@@ -599,7 +621,7 @@ def _hook_identity(command: str) -> str:
|
|
|
599
621
|
text = str(command or "")
|
|
600
622
|
if ".session-start-ts" in text:
|
|
601
623
|
return "session-start-ts"
|
|
602
|
-
match = re.search(r"([A-Za-z0-9._-]+\.sh)\b", text)
|
|
624
|
+
match = re.search(r"([A-Za-z0-9._-]+\.(?:sh|py))\b", text)
|
|
603
625
|
if match:
|
|
604
626
|
return match.group(1)
|
|
605
627
|
return text.strip()
|
package/src/hook_guardrails.py
CHANGED
|
@@ -429,6 +429,38 @@ def _collect_automation_live_repo_blocks(
|
|
|
429
429
|
return blocks
|
|
430
430
|
|
|
431
431
|
|
|
432
|
+
def _read_claude_session_id_from_coordination() -> str:
|
|
433
|
+
"""Fallback claude_session_id when Claude Code's PreToolUse payload omits it.
|
|
434
|
+
|
|
435
|
+
SessionStart hook writes the active Claude Code session UUID to
|
|
436
|
+
``<NEXO_HOME>/coordination/.claude-session-id``. When the PreToolUse
|
|
437
|
+
payload omits ``session_id`` (observed across several Claude Code
|
|
438
|
+
versions), the pre-tool guardrail would lose correlation with the open
|
|
439
|
+
NEXO session and block every write with "unknown target" (learning
|
|
440
|
+
#411). Reading the coordination file restores the correlation without
|
|
441
|
+
relaxing fail-closed semantics: if the file is missing or empty the
|
|
442
|
+
caller still blocks.
|
|
443
|
+
"""
|
|
444
|
+
candidates = []
|
|
445
|
+
nexo_home = os.environ.get("NEXO_HOME", "").strip()
|
|
446
|
+
if nexo_home:
|
|
447
|
+
candidates.append(Path(nexo_home).expanduser() / "coordination" / ".claude-session-id")
|
|
448
|
+
candidates.append(Path.home() / ".nexo" / "coordination" / ".claude-session-id")
|
|
449
|
+
seen: set[str] = set()
|
|
450
|
+
for path in candidates:
|
|
451
|
+
key = str(path)
|
|
452
|
+
if key in seen:
|
|
453
|
+
continue
|
|
454
|
+
seen.add(key)
|
|
455
|
+
try:
|
|
456
|
+
value = path.read_text().strip()
|
|
457
|
+
except (FileNotFoundError, OSError):
|
|
458
|
+
continue
|
|
459
|
+
if value:
|
|
460
|
+
return value
|
|
461
|
+
return ""
|
|
462
|
+
|
|
463
|
+
|
|
432
464
|
def process_pre_tool_event(payload: dict) -> dict:
|
|
433
465
|
tool_name = str(payload.get("tool_name", "")).strip()
|
|
434
466
|
op = _operation_kind(tool_name)
|
|
@@ -439,7 +471,10 @@ def process_pre_tool_event(payload: dict) -> dict:
|
|
|
439
471
|
files = _extract_touched_files(tool_input)
|
|
440
472
|
strictness = get_protocol_strictness()
|
|
441
473
|
conn = get_db()
|
|
442
|
-
|
|
474
|
+
claude_sid = str(payload.get("session_id", "") or "").strip()
|
|
475
|
+
if not claude_sid:
|
|
476
|
+
claude_sid = _read_claude_session_id_from_coordination()
|
|
477
|
+
sid = _resolve_nexo_sid(conn, claude_sid)
|
|
443
478
|
automation_blocks = _collect_automation_live_repo_blocks(
|
|
444
479
|
conn,
|
|
445
480
|
sid=sid,
|
package/src/plugin_loader.py
CHANGED
|
@@ -10,7 +10,17 @@ import time
|
|
|
10
10
|
|
|
11
11
|
from db import get_db
|
|
12
12
|
from fastmcp.tools import Tool
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from tree_hygiene import is_duplicate_artifact_name
|
|
16
|
+
except ModuleNotFoundError as exc:
|
|
17
|
+
if getattr(exc, "name", "") != "tree_hygiene":
|
|
18
|
+
raise
|
|
19
|
+
|
|
20
|
+
# Keep older runtimes bootable long enough to receive tree_hygiene.py
|
|
21
|
+
# during update; duplicate filtering will resume once the module lands.
|
|
22
|
+
def is_duplicate_artifact_name(_path) -> bool:
|
|
23
|
+
return False
|
|
14
24
|
|
|
15
25
|
SERVER_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
16
26
|
PLUGINS_DIR = os.path.join(SERVER_DIR, "plugins")
|
package/src/plugins/update.py
CHANGED
|
@@ -11,7 +11,18 @@ import time
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
from runtime_home import export_resolved_nexo_home
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from tree_hygiene import is_duplicate_artifact_name
|
|
17
|
+
except ModuleNotFoundError as exc:
|
|
18
|
+
if getattr(exc, "name", "") != "tree_hygiene":
|
|
19
|
+
raise
|
|
20
|
+
|
|
21
|
+
# Older packaged runtimes may import update.py before tree_hygiene.py
|
|
22
|
+
# has been copied in. Allow the update to finish so the missing module
|
|
23
|
+
# can be delivered by the same upgrade.
|
|
24
|
+
def is_duplicate_artifact_name(_path) -> bool:
|
|
25
|
+
return False
|
|
15
26
|
|
|
16
27
|
# db_guard landed in v5.5.5. When plugins/update.py is imported from a runtime
|
|
17
28
|
# that still ships the v5.5.4 tree (e.g. mid-upgrade), the import will fail —
|
package/src/resonance_map.py
CHANGED
|
@@ -173,6 +173,10 @@ USER_FACING_CALLERS: dict[str, str] = {
|
|
|
173
173
|
"nexo_chat": USE_USER_DEFAULT_SENTINEL,
|
|
174
174
|
"desktop_new_session": USE_USER_DEFAULT_SENTINEL,
|
|
175
175
|
"nexo_update_interactive": USE_USER_DEFAULT_SENTINEL,
|
|
176
|
+
# v6.0.4 — dashboard "Open followup in Terminal" spawns a fresh
|
|
177
|
+
# interactive Claude/Codex session. Treat it like nexo_chat so the
|
|
178
|
+
# user's default_resonance preference flows through.
|
|
179
|
+
"nexo_followup_terminal": USE_USER_DEFAULT_SENTINEL,
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
# System-owned callers. Grouped thematically for readability.
|
package/src/script_registry.py
CHANGED
|
@@ -304,6 +304,10 @@ def _is_ignored(path: Path) -> bool:
|
|
|
304
304
|
"""Check if file should be ignored entirely."""
|
|
305
305
|
if path.name in _IGNORED_FILES:
|
|
306
306
|
return True
|
|
307
|
+
if re.search(r"\.bak(?:-[\w.-]+)?$", path.name, re.IGNORECASE):
|
|
308
|
+
return True
|
|
309
|
+
if path.name.endswith("~"):
|
|
310
|
+
return True
|
|
307
311
|
if path.name.startswith("."):
|
|
308
312
|
return True
|
|
309
313
|
try:
|