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.
- 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/db/_schema.py +23 -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/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
|
|
|
@@ -2247,6 +2269,7 @@ MIGRATIONS = [
|
|
|
2247
2269
|
(66, "transcript_index", _m66_transcript_index),
|
|
2248
2270
|
(67, "diary_quality_backfill_repair", _m67_diary_quality_backfill_repair),
|
|
2249
2271
|
(68, "memory_fabric_index", _m68_memory_fabric_index),
|
|
2272
|
+
(69, "provider_runtime_metadata", _m69_provider_runtime_metadata),
|
|
2250
2273
|
]
|
|
2251
2274
|
|
|
2252
2275
|
|
package/src/db/_sessions.py
CHANGED
|
@@ -19,6 +19,16 @@ import re
|
|
|
19
19
|
_SID_EXACT = re.compile(r'^nexo-\d+-\d+$')
|
|
20
20
|
_SID_SEARCH = re.compile(r'nexo-\d+-\d+')
|
|
21
21
|
|
|
22
|
+
|
|
23
|
+
def _provider_from_session_client(value: str | None) -> str:
|
|
24
|
+
candidate = str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
|
25
|
+
if candidate in {"anthropic", "claude", "claude_code", "claudecode"}:
|
|
26
|
+
return "anthropic"
|
|
27
|
+
if candidate in {"openai", "codex", "openai_codex"}:
|
|
28
|
+
return "openai"
|
|
29
|
+
return ""
|
|
30
|
+
|
|
31
|
+
|
|
22
32
|
def _validate_sid(sid: str) -> str:
|
|
23
33
|
"""Validate and sanitize SID. Extracts clean SID if embedded in text."""
|
|
24
34
|
if not sid:
|
|
@@ -41,6 +51,7 @@ def register_session(
|
|
|
41
51
|
*,
|
|
42
52
|
external_session_id: str = "",
|
|
43
53
|
session_client: str = "",
|
|
54
|
+
session_provider: str = "",
|
|
44
55
|
conversation_id: str = "",
|
|
45
56
|
) -> dict:
|
|
46
57
|
"""Register or re-register a session."""
|
|
@@ -48,27 +59,49 @@ def register_session(
|
|
|
48
59
|
conn = get_db()
|
|
49
60
|
now = now_epoch()
|
|
50
61
|
linked_session_id = (external_session_id or claude_session_id or "").strip()
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
provider = _provider_from_session_client(session_provider) or _provider_from_session_client(session_client)
|
|
63
|
+
try:
|
|
64
|
+
conn.execute(
|
|
65
|
+
"INSERT OR REPLACE INTO sessions (sid, task, started_epoch, last_update_epoch, local_time, claude_session_id, external_session_id, session_client, session_provider, conversation_id) "
|
|
66
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
67
|
+
(
|
|
68
|
+
sid,
|
|
69
|
+
task,
|
|
70
|
+
now,
|
|
71
|
+
now,
|
|
72
|
+
local_time_str(),
|
|
73
|
+
linked_session_id,
|
|
74
|
+
linked_session_id,
|
|
75
|
+
(session_client or "").strip(),
|
|
76
|
+
provider,
|
|
77
|
+
(conversation_id or "").strip(),
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
except sqlite3.OperationalError as exc:
|
|
81
|
+
if "session_provider" not in str(exc):
|
|
82
|
+
raise
|
|
83
|
+
conn.execute(
|
|
84
|
+
"INSERT OR REPLACE INTO sessions (sid, task, started_epoch, last_update_epoch, local_time, claude_session_id, external_session_id, session_client, conversation_id) "
|
|
85
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
86
|
+
(
|
|
87
|
+
sid,
|
|
88
|
+
task,
|
|
89
|
+
now,
|
|
90
|
+
now,
|
|
91
|
+
local_time_str(),
|
|
92
|
+
linked_session_id,
|
|
93
|
+
linked_session_id,
|
|
94
|
+
(session_client or "").strip(),
|
|
95
|
+
(conversation_id or "").strip(),
|
|
96
|
+
)
|
|
64
97
|
)
|
|
65
|
-
)
|
|
66
98
|
conn.commit()
|
|
67
99
|
return {
|
|
68
100
|
"sid": sid,
|
|
69
101
|
"task": task,
|
|
70
102
|
"external_session_id": linked_session_id,
|
|
71
103
|
"session_client": (session_client or "").strip(),
|
|
104
|
+
"session_provider": provider,
|
|
72
105
|
"conversation_id": (conversation_id or "").strip(),
|
|
73
106
|
}
|
|
74
107
|
|
|
@@ -115,11 +148,20 @@ def get_active_sessions() -> list[dict]:
|
|
|
115
148
|
"""Get all sessions updated within STALE threshold."""
|
|
116
149
|
conn = get_db()
|
|
117
150
|
cutoff = now_epoch() - SESSION_STALE_SECONDS
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
151
|
+
try:
|
|
152
|
+
rows = conn.execute(
|
|
153
|
+
"SELECT sid, task, started_epoch, last_update_epoch, local_time, conversation_id, session_provider "
|
|
154
|
+
"FROM sessions WHERE last_update_epoch > ?",
|
|
155
|
+
(cutoff,)
|
|
156
|
+
).fetchall()
|
|
157
|
+
except sqlite3.OperationalError as exc:
|
|
158
|
+
if "session_provider" not in str(exc):
|
|
159
|
+
raise
|
|
160
|
+
rows = conn.execute(
|
|
161
|
+
"SELECT sid, task, started_epoch, last_update_epoch, local_time, conversation_id "
|
|
162
|
+
"FROM sessions WHERE last_update_epoch > ?",
|
|
163
|
+
(cutoff,)
|
|
164
|
+
).fetchall()
|
|
123
165
|
return [dict(r) for r in rows]
|
|
124
166
|
|
|
125
167
|
|
|
@@ -248,11 +290,20 @@ def search_sessions(keyword: str) -> list[dict]:
|
|
|
248
290
|
"""Find sessions whose task contains keyword (case-insensitive)."""
|
|
249
291
|
conn = get_db()
|
|
250
292
|
cutoff = now_epoch() - SESSION_STALE_SECONDS
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
293
|
+
try:
|
|
294
|
+
rows = conn.execute(
|
|
295
|
+
"SELECT sid, task, last_update_epoch, local_time, conversation_id, session_provider FROM sessions "
|
|
296
|
+
"WHERE last_update_epoch > ? AND LOWER(task) LIKE ?",
|
|
297
|
+
(cutoff, f"%{keyword.lower()}%")
|
|
298
|
+
).fetchall()
|
|
299
|
+
except sqlite3.OperationalError as exc:
|
|
300
|
+
if "session_provider" not in str(exc):
|
|
301
|
+
raise
|
|
302
|
+
rows = conn.execute(
|
|
303
|
+
"SELECT sid, task, last_update_epoch, local_time, conversation_id FROM sessions "
|
|
304
|
+
"WHERE last_update_epoch > ? AND LOWER(task) LIKE ?",
|
|
305
|
+
(cutoff, f"%{keyword.lower()}%")
|
|
306
|
+
).fetchall()
|
|
256
307
|
return [dict(r) for r in rows]
|
|
257
308
|
|
|
258
309
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Provider-runtime contract for Anthropic/Claude Code and OpenAI/Codex."""
|
|
4
|
+
|
|
5
|
+
from client_preferences import (
|
|
6
|
+
BACKEND_NONE,
|
|
7
|
+
CLIENT_CLAUDE_CODE,
|
|
8
|
+
CLIENT_CODEX,
|
|
9
|
+
CLIENT_TO_PROVIDER,
|
|
10
|
+
PROVIDER_ANTHROPIC,
|
|
11
|
+
PROVIDER_NONE,
|
|
12
|
+
PROVIDER_OPENAI,
|
|
13
|
+
PROVIDER_TO_CLIENT,
|
|
14
|
+
client_to_provider,
|
|
15
|
+
default_provider_runtime,
|
|
16
|
+
normalize_provider_key,
|
|
17
|
+
normalize_provider_runtime,
|
|
18
|
+
provider_to_client,
|
|
19
|
+
resolve_automation_provider,
|
|
20
|
+
resolve_selected_chat_provider,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"BACKEND_NONE",
|
|
25
|
+
"CLIENT_CLAUDE_CODE",
|
|
26
|
+
"CLIENT_CODEX",
|
|
27
|
+
"CLIENT_TO_PROVIDER",
|
|
28
|
+
"PROVIDER_ANTHROPIC",
|
|
29
|
+
"PROVIDER_NONE",
|
|
30
|
+
"PROVIDER_OPENAI",
|
|
31
|
+
"PROVIDER_TO_CLIENT",
|
|
32
|
+
"client_to_provider",
|
|
33
|
+
"default_provider_runtime",
|
|
34
|
+
"normalize_provider_key",
|
|
35
|
+
"normalize_provider_runtime",
|
|
36
|
+
"provider_to_client",
|
|
37
|
+
"resolve_automation_provider",
|
|
38
|
+
"resolve_selected_chat_provider",
|
|
39
|
+
]
|
|
@@ -318,6 +318,7 @@ def analyze_session(
|
|
|
318
318
|
output_format="text",
|
|
319
319
|
append_system_prompt=json_system_prompt,
|
|
320
320
|
allowed_tools="Read,Grep,Bash",
|
|
321
|
+
bare_mode=False,
|
|
321
322
|
)
|
|
322
323
|
|
|
323
324
|
if result.returncode != 0:
|
|
@@ -348,6 +349,7 @@ def analyze_session(
|
|
|
348
349
|
timeout=120,
|
|
349
350
|
output_format="text",
|
|
350
351
|
append_system_prompt=json_system_prompt,
|
|
352
|
+
bare_mode=False,
|
|
351
353
|
)
|
|
352
354
|
if convert_result.returncode == 0:
|
|
353
355
|
debug_output = convert_result.stdout
|