nexo-brain 7.25.5 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/bin/nexo-brain.js +233 -29
- package/codex/openai-codex-0.133.0.tgz +0 -0
- package/package.json +7 -1
- package/src/agent_runner.py +96 -31
- package/src/cli.py +117 -4
- package/src/client_preferences.py +293 -1
- package/src/client_sync.py +327 -1
- package/src/crons/sync.py +42 -18
- package/src/db/_schema.py +53 -0
- package/src/db/_sessions.py +75 -24
- package/src/provider_runtime.py +39 -0
- package/src/scripts/deep-sleep/extract.py +2 -0
- package/src/scripts/deep-sleep/synthesize.py +1 -0
- package/src/scripts/nexo-cron-wrapper.sh +108 -25
- package/src/scripts/nexo-morning-agent.py +1 -1
- package/src/server.py +3 -1
- package/src/tools_automation_sessions.py +2 -1
- package/src/tools_sessions.py +13 -8
package/src/client_sync.py
CHANGED
|
@@ -10,6 +10,7 @@ import shlex
|
|
|
10
10
|
import shutil
|
|
11
11
|
import subprocess
|
|
12
12
|
import sys
|
|
13
|
+
import tarfile
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
from calibration_runtime import load_runtime_calibration
|
|
@@ -80,6 +81,7 @@ except Exception:
|
|
|
80
81
|
|
|
81
82
|
|
|
82
83
|
CLAUDE_CODE_NPM_PACKAGE = "@anthropic-ai/claude-code"
|
|
84
|
+
CODEX_NPM_PACKAGE = "@openai/codex"
|
|
83
85
|
DEFAULT_ASSISTANT_NAME = "Nova"
|
|
84
86
|
HOOK_TIMEOUTS_BY_EVENT = {
|
|
85
87
|
"SessionStart": 40,
|
|
@@ -235,6 +237,159 @@ def _managed_claude_prefix(user_home: Path | None = None) -> Path:
|
|
|
235
237
|
return home / ".nexo" / "runtime" / "bootstrap" / "npm-global"
|
|
236
238
|
|
|
237
239
|
|
|
240
|
+
def _brain_bundle_root() -> Path:
|
|
241
|
+
return Path(__file__).resolve().parents[1]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _platform_slug() -> str:
|
|
245
|
+
if sys.platform.startswith("darwin"):
|
|
246
|
+
os_part = "darwin"
|
|
247
|
+
elif sys.platform.startswith("linux"):
|
|
248
|
+
os_part = "linux"
|
|
249
|
+
elif sys.platform.startswith("win"):
|
|
250
|
+
os_part = "win32"
|
|
251
|
+
else:
|
|
252
|
+
os_part = sys.platform
|
|
253
|
+
machine = __import__("platform").machine().lower()
|
|
254
|
+
if machine in {"x86_64", "amd64"}:
|
|
255
|
+
arch = "x64"
|
|
256
|
+
elif machine in {"aarch64", "arm64"}:
|
|
257
|
+
arch = "arm64"
|
|
258
|
+
else:
|
|
259
|
+
arch = machine
|
|
260
|
+
return f"{os_part}-{arch}"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _install_npm_package_from_bundle(
|
|
264
|
+
*,
|
|
265
|
+
bundle_dir: Path,
|
|
266
|
+
wrapper_pattern: str,
|
|
267
|
+
package_name: str,
|
|
268
|
+
managed_prefix: Path,
|
|
269
|
+
env: dict[str, str],
|
|
270
|
+
attempts: list[str],
|
|
271
|
+
) -> bool:
|
|
272
|
+
if not bundle_dir.is_dir():
|
|
273
|
+
return False
|
|
274
|
+
all_tgz = sorted(item for item in bundle_dir.iterdir() if item.name.endswith(".tgz"))
|
|
275
|
+
wrapper_re = re.compile(wrapper_pattern)
|
|
276
|
+
wrapper = next((item for item in all_tgz if wrapper_re.match(item.name)), None)
|
|
277
|
+
if not wrapper:
|
|
278
|
+
return False
|
|
279
|
+
slug = _platform_slug()
|
|
280
|
+
native_packs = [
|
|
281
|
+
item
|
|
282
|
+
for item in all_tgz
|
|
283
|
+
if item != wrapper and (f"-{slug}.tgz" in item.name or f"-{slug}-" in item.name)
|
|
284
|
+
]
|
|
285
|
+
tgz_paths = [wrapper, *native_packs] if native_packs else [wrapper]
|
|
286
|
+
desktop_node, bundled_npm_cli = _bundled_npm_runtime()
|
|
287
|
+
if desktop_node and bundled_npm_cli:
|
|
288
|
+
cmd = [
|
|
289
|
+
desktop_node,
|
|
290
|
+
bundled_npm_cli,
|
|
291
|
+
"install",
|
|
292
|
+
"-g",
|
|
293
|
+
"--prefix",
|
|
294
|
+
str(managed_prefix),
|
|
295
|
+
"--offline",
|
|
296
|
+
"--no-audit",
|
|
297
|
+
"--no-fund",
|
|
298
|
+
*(str(item) for item in tgz_paths),
|
|
299
|
+
]
|
|
300
|
+
run_env = {**env, "ELECTRON_RUN_AS_NODE": "1"}
|
|
301
|
+
else:
|
|
302
|
+
cmd = [
|
|
303
|
+
"npm",
|
|
304
|
+
"install",
|
|
305
|
+
"-g",
|
|
306
|
+
"--prefix",
|
|
307
|
+
str(managed_prefix),
|
|
308
|
+
"--offline",
|
|
309
|
+
"--no-audit",
|
|
310
|
+
"--no-fund",
|
|
311
|
+
*(str(item) for item in tgz_paths),
|
|
312
|
+
]
|
|
313
|
+
run_env = env
|
|
314
|
+
try:
|
|
315
|
+
install = subprocess.run(
|
|
316
|
+
cmd,
|
|
317
|
+
capture_output=True,
|
|
318
|
+
text=True,
|
|
319
|
+
timeout=300,
|
|
320
|
+
env=run_env,
|
|
321
|
+
)
|
|
322
|
+
if install.returncode == 0:
|
|
323
|
+
return True
|
|
324
|
+
attempts.append((install.stderr or install.stdout or f"{package_name} bundled install failed").strip())
|
|
325
|
+
except Exception as exc:
|
|
326
|
+
attempts.append(f"{package_name} bundled install failed: {exc}")
|
|
327
|
+
return False
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _install_codex_vendor_from_bundle(*, bundle_dir: Path, managed_prefix: Path, attempts: list[str]) -> bool:
|
|
331
|
+
package_roots = [
|
|
332
|
+
managed_prefix / "lib" / "node_modules" / "@openai" / "codex",
|
|
333
|
+
managed_prefix / "node_modules" / "@openai" / "codex",
|
|
334
|
+
]
|
|
335
|
+
package_root = next((item for item in package_roots if item.exists()), package_roots[0])
|
|
336
|
+
if not package_root.exists() or not bundle_dir.is_dir():
|
|
337
|
+
return False
|
|
338
|
+
slug = _platform_slug()
|
|
339
|
+
native_packs = sorted(
|
|
340
|
+
item for item in bundle_dir.iterdir()
|
|
341
|
+
if item.name.endswith(".tgz") and (f"-{slug}.tgz" in item.name or f"-{slug}-" in item.name)
|
|
342
|
+
)
|
|
343
|
+
if not native_packs:
|
|
344
|
+
attempts.append(f"no bundled Codex native vendor found for {slug}")
|
|
345
|
+
return False
|
|
346
|
+
vendor_dest = package_root / "vendor"
|
|
347
|
+
vendor_dest.mkdir(parents=True, exist_ok=True)
|
|
348
|
+
for pack in native_packs:
|
|
349
|
+
try:
|
|
350
|
+
with tarfile.open(pack, "r:gz") as archive:
|
|
351
|
+
members = [member for member in archive.getmembers() if member.name.startswith("package/vendor/")]
|
|
352
|
+
for member in members:
|
|
353
|
+
relative = Path(member.name).relative_to("package/vendor")
|
|
354
|
+
target = vendor_dest / relative
|
|
355
|
+
if member.isdir():
|
|
356
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
357
|
+
continue
|
|
358
|
+
if not member.isfile():
|
|
359
|
+
continue
|
|
360
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
361
|
+
source = archive.extractfile(member)
|
|
362
|
+
if source is None:
|
|
363
|
+
continue
|
|
364
|
+
with source, target.open("wb") as out:
|
|
365
|
+
shutil.copyfileobj(source, out)
|
|
366
|
+
try:
|
|
367
|
+
target.chmod(member.mode)
|
|
368
|
+
except Exception:
|
|
369
|
+
pass
|
|
370
|
+
return True
|
|
371
|
+
except Exception as exc:
|
|
372
|
+
attempts.append(f"Codex native vendor extract failed for {pack.name}: {exc}")
|
|
373
|
+
return False
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _codex_vendor_present(managed_prefix: Path) -> bool:
|
|
377
|
+
package_roots = [
|
|
378
|
+
managed_prefix / "lib" / "node_modules" / "@openai" / "codex",
|
|
379
|
+
managed_prefix / "node_modules" / "@openai" / "codex",
|
|
380
|
+
]
|
|
381
|
+
for package_root in package_roots:
|
|
382
|
+
vendor_root = package_root / "vendor"
|
|
383
|
+
if not vendor_root.exists():
|
|
384
|
+
continue
|
|
385
|
+
try:
|
|
386
|
+
if any(candidate.is_file() for candidate in vendor_root.rglob("bin/codex*")):
|
|
387
|
+
return True
|
|
388
|
+
except Exception:
|
|
389
|
+
continue
|
|
390
|
+
return False
|
|
391
|
+
|
|
392
|
+
|
|
238
393
|
def _desktop_product_requested(user_home: Path | None = None) -> bool:
|
|
239
394
|
if str(os.environ.get("NEXO_DESKTOP_MANAGED", "")).strip() == "1":
|
|
240
395
|
return True
|
|
@@ -289,6 +444,13 @@ def _installed_client_path(client_key: str, *, user_home: Path | None = None) ->
|
|
|
289
444
|
return ""
|
|
290
445
|
|
|
291
446
|
|
|
447
|
+
def _sync_codex_binary(home_path: Path) -> str:
|
|
448
|
+
if _desktop_product_requested(home_path):
|
|
449
|
+
info = detect_installed_clients(user_home=home_path).get("codex", {})
|
|
450
|
+
return str(info.get("path") or "") if info.get("installed") else ""
|
|
451
|
+
return shutil.which("codex") or ""
|
|
452
|
+
|
|
453
|
+
|
|
292
454
|
def ensure_claude_code_installed(*, user_home: str | os.PathLike[str] | None = None) -> dict:
|
|
293
455
|
home_path = Path(user_home).expanduser() if user_home else _user_home()
|
|
294
456
|
desktop_managed = _desktop_product_requested(home_path)
|
|
@@ -471,6 +633,167 @@ def ensure_claude_code_installed(*, user_home: str | os.PathLike[str] | None = N
|
|
|
471
633
|
}
|
|
472
634
|
|
|
473
635
|
|
|
636
|
+
def ensure_codex_installed(*, user_home: str | os.PathLike[str] | None = None) -> dict:
|
|
637
|
+
home_path = Path(user_home).expanduser() if user_home else _user_home()
|
|
638
|
+
desktop_managed = _desktop_product_requested(home_path)
|
|
639
|
+
managed_prefix = _managed_claude_prefix(home_path)
|
|
640
|
+
existing = _installed_client_path("codex", user_home=home_path)
|
|
641
|
+
if existing and (not desktop_managed or _codex_vendor_present(managed_prefix)):
|
|
642
|
+
return {
|
|
643
|
+
"ok": True,
|
|
644
|
+
"client": "codex",
|
|
645
|
+
"installed": True,
|
|
646
|
+
"changed": False,
|
|
647
|
+
"action": "already_installed_managed" if desktop_managed else "already_installed",
|
|
648
|
+
"path": existing,
|
|
649
|
+
"attempts": [],
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
attempts: list[str] = []
|
|
653
|
+
if existing and desktop_managed:
|
|
654
|
+
attempts.append("managed Codex wrapper exists but native vendor is missing; repairing bundled vendor")
|
|
655
|
+
env = _cli_install_env(home_path)
|
|
656
|
+
env.setdefault("npm_config_prefix", str(managed_prefix))
|
|
657
|
+
env["PATH"] = os.pathsep.join(
|
|
658
|
+
[str(managed_prefix / "bin"), *(item for item in str(env.get("PATH", "")).split(os.pathsep) if item)]
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
desktop_node, bundled_npm_cli = _bundled_npm_runtime()
|
|
662
|
+
bundle_dir = _brain_bundle_root() / "codex"
|
|
663
|
+
if desktop_managed and not (desktop_node and bundled_npm_cli):
|
|
664
|
+
vendor_installed = _install_codex_vendor_from_bundle(
|
|
665
|
+
bundle_dir=bundle_dir,
|
|
666
|
+
managed_prefix=managed_prefix,
|
|
667
|
+
attempts=attempts,
|
|
668
|
+
)
|
|
669
|
+
installed_after_vendor = _installed_client_path("codex", user_home=home_path)
|
|
670
|
+
if installed_after_vendor and vendor_installed:
|
|
671
|
+
return {
|
|
672
|
+
"ok": True,
|
|
673
|
+
"client": "codex",
|
|
674
|
+
"installed": True,
|
|
675
|
+
"changed": True,
|
|
676
|
+
"action": "installed_bundled_vendor",
|
|
677
|
+
"path": installed_after_vendor,
|
|
678
|
+
"attempts": attempts,
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
"ok": False,
|
|
682
|
+
"client": "codex",
|
|
683
|
+
"installed": False,
|
|
684
|
+
"changed": False,
|
|
685
|
+
"action": "failed",
|
|
686
|
+
"path": "",
|
|
687
|
+
"attempts": attempts,
|
|
688
|
+
"error": (
|
|
689
|
+
"Desktop-managed Codex install requires the NEXO Desktop bundled Codex runtime; "
|
|
690
|
+
"global `npm -g` fallbacks are disabled."
|
|
691
|
+
),
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
wrapper_installed = _install_npm_package_from_bundle(
|
|
695
|
+
bundle_dir=bundle_dir,
|
|
696
|
+
wrapper_pattern=r"^openai-codex-\d+\.\d+\.\d+\.tgz$",
|
|
697
|
+
package_name=CODEX_NPM_PACKAGE,
|
|
698
|
+
managed_prefix=managed_prefix,
|
|
699
|
+
env=env,
|
|
700
|
+
attempts=attempts,
|
|
701
|
+
)
|
|
702
|
+
vendor_installed = _install_codex_vendor_from_bundle(bundle_dir=bundle_dir, managed_prefix=managed_prefix, attempts=attempts)
|
|
703
|
+
installed_after_bundle = _installed_client_path("codex", user_home=home_path)
|
|
704
|
+
if installed_after_bundle and vendor_installed:
|
|
705
|
+
return {
|
|
706
|
+
"ok": True,
|
|
707
|
+
"client": "codex",
|
|
708
|
+
"installed": True,
|
|
709
|
+
"changed": True,
|
|
710
|
+
"action": "installed_via_bundled_tarballs",
|
|
711
|
+
"path": installed_after_bundle,
|
|
712
|
+
"attempts": attempts,
|
|
713
|
+
}
|
|
714
|
+
if wrapper_installed and not vendor_installed:
|
|
715
|
+
attempts.append("bundled Codex wrapper installed but native vendor extraction failed; falling back to online install")
|
|
716
|
+
|
|
717
|
+
if desktop_node and bundled_npm_cli:
|
|
718
|
+
try:
|
|
719
|
+
install = subprocess.run(
|
|
720
|
+
[
|
|
721
|
+
desktop_node,
|
|
722
|
+
bundled_npm_cli,
|
|
723
|
+
"install",
|
|
724
|
+
"-g",
|
|
725
|
+
"--prefix",
|
|
726
|
+
str(managed_prefix),
|
|
727
|
+
CODEX_NPM_PACKAGE,
|
|
728
|
+
],
|
|
729
|
+
capture_output=True,
|
|
730
|
+
text=True,
|
|
731
|
+
timeout=300,
|
|
732
|
+
env={**env, "ELECTRON_RUN_AS_NODE": "1"},
|
|
733
|
+
)
|
|
734
|
+
if install.returncode != 0:
|
|
735
|
+
attempts.append((install.stderr or install.stdout or "bundled npm install failed").strip())
|
|
736
|
+
except Exception as exc:
|
|
737
|
+
attempts.append(f"bundled npm install failed: {exc}")
|
|
738
|
+
installed_after_npm = _installed_client_path("codex", user_home=home_path)
|
|
739
|
+
if installed_after_npm and (not desktop_managed or _codex_vendor_present(managed_prefix)):
|
|
740
|
+
return {
|
|
741
|
+
"ok": True,
|
|
742
|
+
"client": "codex",
|
|
743
|
+
"installed": True,
|
|
744
|
+
"changed": True,
|
|
745
|
+
"action": "installed_via_bundled_npm",
|
|
746
|
+
"path": installed_after_npm,
|
|
747
|
+
"attempts": attempts,
|
|
748
|
+
}
|
|
749
|
+
if installed_after_npm and desktop_managed:
|
|
750
|
+
attempts.append("managed Codex wrapper installed but native vendor is missing after bundled npm install")
|
|
751
|
+
|
|
752
|
+
if desktop_managed:
|
|
753
|
+
error = (
|
|
754
|
+
"Desktop-managed Codex install did not produce the managed "
|
|
755
|
+
"`~/.nexo/runtime/bootstrap/npm-global/bin/codex` binary."
|
|
756
|
+
)
|
|
757
|
+
return {
|
|
758
|
+
"ok": False,
|
|
759
|
+
"client": "codex",
|
|
760
|
+
"installed": False,
|
|
761
|
+
"changed": False,
|
|
762
|
+
"action": "failed",
|
|
763
|
+
"path": "",
|
|
764
|
+
"attempts": attempts,
|
|
765
|
+
"error": error,
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
install_error = ""
|
|
769
|
+
try:
|
|
770
|
+
install = subprocess.run(
|
|
771
|
+
["npm", "install", "-g", "--prefix", str(managed_prefix), CODEX_NPM_PACKAGE],
|
|
772
|
+
capture_output=True,
|
|
773
|
+
text=True,
|
|
774
|
+
timeout=180,
|
|
775
|
+
env=env,
|
|
776
|
+
)
|
|
777
|
+
if install.returncode != 0:
|
|
778
|
+
install_error = (install.stderr or install.stdout or "npm install failed").strip()
|
|
779
|
+
attempts.append(install_error)
|
|
780
|
+
except Exception as exc:
|
|
781
|
+
install_error = f"npm install failed: {exc}"
|
|
782
|
+
attempts.append(install_error)
|
|
783
|
+
|
|
784
|
+
installed_path = _installed_client_path("codex", user_home=home_path)
|
|
785
|
+
return {
|
|
786
|
+
"ok": bool(installed_path),
|
|
787
|
+
"client": "codex",
|
|
788
|
+
"installed": bool(installed_path),
|
|
789
|
+
"changed": bool(installed_path),
|
|
790
|
+
"action": "installed" if installed_path else "failed",
|
|
791
|
+
"path": installed_path or "",
|
|
792
|
+
"attempts": attempts,
|
|
793
|
+
**({} if installed_path else {"error": install_error or "Codex install did not produce a `codex` binary in PATH"}),
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
|
|
474
797
|
def build_server_config(
|
|
475
798
|
*,
|
|
476
799
|
nexo_home: str | os.PathLike[str] | None = None,
|
|
@@ -1334,7 +1657,7 @@ def sync_codex(
|
|
|
1334
1657
|
operator_name=operator_name,
|
|
1335
1658
|
client="codex",
|
|
1336
1659
|
)
|
|
1337
|
-
codex_bin =
|
|
1660
|
+
codex_bin = _sync_codex_binary(home_path)
|
|
1338
1661
|
config_path = _codex_config_path(home_path)
|
|
1339
1662
|
hooks_path = _codex_hooks_path(home_path)
|
|
1340
1663
|
if not codex_bin:
|
|
@@ -1423,6 +1746,7 @@ def sync_all_clients(
|
|
|
1423
1746
|
enabled_clients: list[str] | tuple[str, ...] | set[str] | None = None,
|
|
1424
1747
|
preferences: dict | None = None,
|
|
1425
1748
|
auto_install_missing_claude: bool = False,
|
|
1749
|
+
auto_install_missing_codex: bool = False,
|
|
1426
1750
|
) -> dict:
|
|
1427
1751
|
nexo_home_path = Path(nexo_home).expanduser() if nexo_home else _default_nexo_home()
|
|
1428
1752
|
guardian_runtime_surfaces_result: dict = {
|
|
@@ -1469,6 +1793,8 @@ def sync_all_clients(
|
|
|
1469
1793
|
install_results: dict[str, dict] = {}
|
|
1470
1794
|
if auto_install_missing_claude and "claude_code" in enabled_set:
|
|
1471
1795
|
install_results["claude_code"] = ensure_claude_code_installed(user_home=user_home)
|
|
1796
|
+
if auto_install_missing_codex and "codex" in enabled_set:
|
|
1797
|
+
install_results["codex"] = ensure_codex_installed(user_home=user_home)
|
|
1472
1798
|
|
|
1473
1799
|
def _safe(label: str, fn) -> dict:
|
|
1474
1800
|
if label not in enabled_set:
|
package/src/crons/sync.py
CHANGED
|
@@ -150,6 +150,41 @@ RETIRED_CORE_FILES = (
|
|
|
150
150
|
)
|
|
151
151
|
|
|
152
152
|
|
|
153
|
+
def _resolve_core_python_bin() -> str:
|
|
154
|
+
"""Prefer the NEXO-managed Python for core cron execution."""
|
|
155
|
+
candidates = [
|
|
156
|
+
os.environ.get("NEXO_RUNTIME_PYTHON", ""),
|
|
157
|
+
os.environ.get("NEXO_PYTHON", ""),
|
|
158
|
+
str(RUNTIME_ROOT / ".venv" / "bin" / "python3"),
|
|
159
|
+
str(RUNTIME_ROOT / ".venv" / "bin" / "python"),
|
|
160
|
+
str(_runtime_code_dir() / ".venv" / "bin" / "python3"),
|
|
161
|
+
str(_runtime_code_dir() / ".venv" / "bin" / "python"),
|
|
162
|
+
]
|
|
163
|
+
if platform.system() == "Darwin":
|
|
164
|
+
candidates.extend(
|
|
165
|
+
[
|
|
166
|
+
"/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
|
|
167
|
+
"/opt/homebrew/bin/python3.12",
|
|
168
|
+
"/usr/local/bin/python3.12",
|
|
169
|
+
"/opt/homebrew/bin/python3",
|
|
170
|
+
"/usr/local/bin/python3",
|
|
171
|
+
"/usr/bin/python3",
|
|
172
|
+
]
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
candidates.extend(["/usr/bin/python3", "/usr/local/bin/python3", "python3"])
|
|
176
|
+
|
|
177
|
+
for candidate in candidates:
|
|
178
|
+
if not candidate:
|
|
179
|
+
continue
|
|
180
|
+
expanded = Path(str(candidate)).expanduser()
|
|
181
|
+
if expanded.exists():
|
|
182
|
+
return str(expanded)
|
|
183
|
+
if os.sep not in str(candidate) and shutil.which(str(candidate)):
|
|
184
|
+
return str(candidate)
|
|
185
|
+
return "python3"
|
|
186
|
+
|
|
187
|
+
|
|
153
188
|
def _runtime_scripts_dir() -> Path:
|
|
154
189
|
new = RUNTIME_ROOT / "core" / "scripts"
|
|
155
190
|
legacy = RUNTIME_ROOT / "scripts"
|
|
@@ -407,21 +442,10 @@ def build_plist(cron: dict) -> dict:
|
|
|
407
442
|
if subdir_src.is_dir():
|
|
408
443
|
_copy_into_runtime(subdir_src)
|
|
409
444
|
|
|
445
|
+
python_bin = _resolve_core_python_bin()
|
|
410
446
|
if script_type == "shell":
|
|
411
447
|
program_args = ["/bin/bash", wrapper_path, cron_id, "/bin/bash", script_path]
|
|
412
448
|
else:
|
|
413
|
-
# Find python3
|
|
414
|
-
python_candidates = [
|
|
415
|
-
"/opt/homebrew/bin/python3",
|
|
416
|
-
"/usr/local/bin/python3",
|
|
417
|
-
"/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
|
|
418
|
-
"/usr/bin/python3",
|
|
419
|
-
]
|
|
420
|
-
python_bin = "python3"
|
|
421
|
-
for p in python_candidates:
|
|
422
|
-
if Path(p).exists():
|
|
423
|
-
python_bin = p
|
|
424
|
-
break
|
|
425
449
|
program_args = ["/bin/bash", wrapper_path, cron_id, python_bin, script_path]
|
|
426
450
|
|
|
427
451
|
plist = {
|
|
@@ -436,6 +460,7 @@ def build_plist(cron: dict) -> dict:
|
|
|
436
460
|
"NEXO_CODE": str(_runtime_code_dir()),
|
|
437
461
|
"NEXO_SOURCE_CODE": str(SOURCE_ROOT),
|
|
438
462
|
"NEXO_MANAGED_CORE_CRON": "1",
|
|
463
|
+
"NEXO_RUNTIME_PYTHON": python_bin,
|
|
439
464
|
"PYTHONUNBUFFERED": "1",
|
|
440
465
|
},
|
|
441
466
|
}
|
|
@@ -505,6 +530,7 @@ def _linux_crontab_entry(cron: dict, exec_cmd: str, stdout_log: Path, stderr_log
|
|
|
505
530
|
"HOME": Path.home(),
|
|
506
531
|
"NEXO_HOME": NEXO_HOME,
|
|
507
532
|
"NEXO_CODE": _runtime_code_dir(),
|
|
533
|
+
"NEXO_RUNTIME_PYTHON": _resolve_core_python_bin(),
|
|
508
534
|
"PYTHONUNBUFFERED": "1",
|
|
509
535
|
}.items()
|
|
510
536
|
)
|
|
@@ -578,12 +604,13 @@ def _sync_wsl_windows_host_local_index_task(dry_run: bool = False) -> dict:
|
|
|
578
604
|
log("WARNING: WSL_DISTRO_NAME missing; local-index host task not installed.")
|
|
579
605
|
return {"ok": False, "skipped": True, "reason": "wsl_distro_missing"}
|
|
580
606
|
|
|
581
|
-
python_bin =
|
|
607
|
+
python_bin = _resolve_core_python_bin()
|
|
582
608
|
script_path = _runtime_code_dir() / "scripts" / "nexo-local-index.py"
|
|
583
609
|
command = (
|
|
584
610
|
f"cd {shlex.quote(str(Path.home()))} && "
|
|
585
611
|
f"NEXO_HOME={shlex.quote(str(NEXO_HOME))} "
|
|
586
612
|
f"NEXO_CODE={shlex.quote(str(_runtime_code_dir()))} "
|
|
613
|
+
f"NEXO_RUNTIME_PYTHON={shlex.quote(python_bin)} "
|
|
587
614
|
f"{shlex.quote(python_bin)} {shlex.quote(str(script_path))}"
|
|
588
615
|
)
|
|
589
616
|
wsl_args = " ".join(
|
|
@@ -835,11 +862,7 @@ def sync_linux(dry_run: bool = False):
|
|
|
835
862
|
|
|
836
863
|
log(f"Manifest: {len(manifest_crons)} core crons")
|
|
837
864
|
|
|
838
|
-
python_bin =
|
|
839
|
-
for p in ["/usr/bin/python3", "/usr/local/bin/python3"]:
|
|
840
|
-
if Path(p).exists():
|
|
841
|
-
python_bin = p
|
|
842
|
-
break
|
|
865
|
+
python_bin = _resolve_core_python_bin()
|
|
843
866
|
|
|
844
867
|
enable_units: list[str] = []
|
|
845
868
|
crontab_entries: list[str] = []
|
|
@@ -878,6 +901,7 @@ Type={service_type}
|
|
|
878
901
|
ExecStart={exec_cmd}
|
|
879
902
|
Environment=NEXO_HOME={NEXO_HOME}
|
|
880
903
|
Environment=NEXO_CODE={_runtime_code_dir()}
|
|
904
|
+
Environment=NEXO_RUNTIME_PYTHON={python_bin}
|
|
881
905
|
Environment=HOME={Path.home()}
|
|
882
906
|
StandardOutput=append:{stdout_log}
|
|
883
907
|
StandardError=append:{stderr_log}
|
package/src/db/_schema.py
CHANGED
|
@@ -1117,6 +1117,28 @@ def _m41_automation_sessions_columns(conn):
|
|
|
1117
1117
|
)
|
|
1118
1118
|
|
|
1119
1119
|
|
|
1120
|
+
def _m69_provider_runtime_metadata(conn):
|
|
1121
|
+
"""Add provider/runtime metadata required for Anthropic/OpenAI parity."""
|
|
1122
|
+
if not _table_exists(conn, "automation_runs"):
|
|
1123
|
+
_m28_automation_runs(conn)
|
|
1124
|
+
if not _table_exists(conn, "cron_runs"):
|
|
1125
|
+
_m17_cron_runs(conn)
|
|
1126
|
+
|
|
1127
|
+
if _table_exists(conn, "automation_runs"):
|
|
1128
|
+
_migrate_add_column(conn, "automation_runs", "provider", "TEXT DEFAULT ''")
|
|
1129
|
+
_migrate_add_column(conn, "automation_runs", "runtime_version", "TEXT DEFAULT ''")
|
|
1130
|
+
_migrate_add_column(conn, "automation_runs", "runtime_session_id", "TEXT DEFAULT ''")
|
|
1131
|
+
_migrate_add_index(conn, "idx_automation_runs_provider", "automation_runs", "provider")
|
|
1132
|
+
if _table_exists(conn, "cron_runs"):
|
|
1133
|
+
_migrate_add_column(conn, "cron_runs", "provider", "TEXT DEFAULT ''")
|
|
1134
|
+
_migrate_add_column(conn, "cron_runs", "backend", "TEXT DEFAULT ''")
|
|
1135
|
+
_migrate_add_column(conn, "cron_runs", "runtime_snapshot", "TEXT DEFAULT '{}'")
|
|
1136
|
+
_migrate_add_index(conn, "idx_cron_runs_provider", "cron_runs", "provider")
|
|
1137
|
+
if _table_exists(conn, "sessions"):
|
|
1138
|
+
_migrate_add_column(conn, "sessions", "session_provider", "TEXT DEFAULT ''")
|
|
1139
|
+
_migrate_add_index(conn, "idx_sessions_provider", "sessions", "session_provider")
|
|
1140
|
+
|
|
1141
|
+
|
|
1120
1142
|
def _m42_v6_0_1_hotfix(conn):
|
|
1121
1143
|
"""v6.0.1 hotfix — last_heartbeat_ts on sessions + hook_inbox_reminders.
|
|
1122
1144
|
|
|
@@ -1767,6 +1789,7 @@ def _m62_memory_observations_fts_trigger_fix(conn):
|
|
|
1767
1789
|
|
|
1768
1790
|
def _m63_local_context_layer(conn):
|
|
1769
1791
|
"""Local Context Layer storage for on-device memory indexing."""
|
|
1792
|
+
_m63_repair_legacy_local_context_columns(conn)
|
|
1770
1793
|
conn.executescript(
|
|
1771
1794
|
"""
|
|
1772
1795
|
CREATE TABLE IF NOT EXISTS local_index_roots (
|
|
@@ -1995,6 +2018,35 @@ def _m63_local_context_layer(conn):
|
|
|
1995
2018
|
)
|
|
1996
2019
|
|
|
1997
2020
|
|
|
2021
|
+
def _table_exists(conn, table: str) -> bool:
|
|
2022
|
+
row = conn.execute(
|
|
2023
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1",
|
|
2024
|
+
(table,),
|
|
2025
|
+
).fetchone()
|
|
2026
|
+
return bool(row)
|
|
2027
|
+
|
|
2028
|
+
|
|
2029
|
+
def _m63_repair_legacy_local_context_columns(conn):
|
|
2030
|
+
"""Add v2 columns before m63 creates indexes that reference them.
|
|
2031
|
+
|
|
2032
|
+
Existing sidecar DBs can already have m63-era tables without the v2
|
|
2033
|
+
columns. CREATE TABLE IF NOT EXISTS will not alter those tables, so index
|
|
2034
|
+
creation must be preceded by additive repairs.
|
|
2035
|
+
"""
|
|
2036
|
+
if _table_exists(conn, "local_index_roots"):
|
|
2037
|
+
_migrate_add_column(conn, "local_index_roots", "source", "TEXT NOT NULL DEFAULT 'legacy'")
|
|
2038
|
+
_migrate_add_column(conn, "local_index_roots", "remote", "INTEGER NOT NULL DEFAULT 0")
|
|
2039
|
+
_migrate_add_column(conn, "local_index_roots", "seed_version", "INTEGER NOT NULL DEFAULT 1")
|
|
2040
|
+
if _table_exists(conn, "local_index_exclusions"):
|
|
2041
|
+
_migrate_add_column(conn, "local_index_exclusions", "source", "TEXT NOT NULL DEFAULT 'legacy'")
|
|
2042
|
+
_migrate_add_column(conn, "local_index_exclusions", "kind", "TEXT NOT NULL DEFAULT 'folder'")
|
|
2043
|
+
if _table_exists(conn, "local_index_file_type_rules"):
|
|
2044
|
+
_migrate_add_column(conn, "local_index_file_type_rules", "source", "TEXT NOT NULL DEFAULT 'legacy'")
|
|
2045
|
+
_migrate_add_column(conn, "local_index_file_type_rules", "priority", "INTEGER NOT NULL DEFAULT 0")
|
|
2046
|
+
_migrate_add_column(conn, "local_index_file_type_rules", "reason", "TEXT NOT NULL DEFAULT ''")
|
|
2047
|
+
_migrate_add_column(conn, "local_index_file_type_rules", "updated_at", "REAL NOT NULL DEFAULT 0")
|
|
2048
|
+
|
|
2049
|
+
|
|
1998
2050
|
def _m64_local_context_live_dirs(conn):
|
|
1999
2051
|
"""Track known folders so local context can detect new/deleted/changed files quickly."""
|
|
2000
2052
|
conn.executescript(
|
|
@@ -2217,6 +2269,7 @@ MIGRATIONS = [
|
|
|
2217
2269
|
(66, "transcript_index", _m66_transcript_index),
|
|
2218
2270
|
(67, "diary_quality_backfill_repair", _m67_diary_quality_backfill_repair),
|
|
2219
2271
|
(68, "memory_fabric_index", _m68_memory_fabric_index),
|
|
2272
|
+
(69, "provider_runtime_metadata", _m69_provider_runtime_metadata),
|
|
2220
2273
|
]
|
|
2221
2274
|
|
|
2222
2275
|
|