nexo-brain 7.18.0 → 7.19.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 +5 -1
- package/bin/nexo-brain.js +1 -1
- package/package.json +1 -1
- package/src/auto_update.py +6 -1
- package/src/local_context/api.py +191 -14
- package/src/local_context/privacy.py +23 -0
- package/src/runtime_versioning.py +31 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.19.0",
|
|
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,11 @@
|
|
|
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 `7.
|
|
21
|
+
Version `7.19.0` is the current packaged-runtime line. Minor release over v7.18.1 — bundle-managed installations (NEXO Desktop `brain-bundle/`) can now pin Brain to the host application release cycle via `NEXO_BRAIN_AUTO_UPDATE=false`, and the server auto-exits with code 75 on fingerprint mismatch so MCP clients respawn the server with the new code instead of leaving stale `server.py` processes alive.
|
|
22
|
+
|
|
23
|
+
Previously in `7.18.1`: patch release over v7.18.0 - packaged Brain runtimes now include the `local_context` package, so Desktop Local Memory and `nexo local-context` do not get stuck behind `ModuleNotFoundError` or zero-file status; the local-index service also keeps detecting newly mounted volumes automatically.
|
|
24
|
+
|
|
25
|
+
Previously in `7.18.0`: minor release over v7.17.8 - Brain adds the Local Context Layer: a local-only background memory index with checkpoints, extraction, graph links, embeddings, MCP/CLI controls, and pre-action evidence for NEXO agents.
|
|
22
26
|
|
|
23
27
|
Previously in `7.17.8`: patch release over v7.17.7 - standalone `nexo chat` now surfaces macOS Full Disk Access guidance, and Brain clears stale permission state after a live access probe succeeds.
|
|
24
28
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -1123,7 +1123,7 @@ function getCoreRuntimeFlatFiles(srcDir = path.join(__dirname, "..", "src")) {
|
|
|
1123
1123
|
}
|
|
1124
1124
|
|
|
1125
1125
|
function getCoreRuntimePackages() {
|
|
1126
|
-
return ["db", "cognitive", "doctor"];
|
|
1126
|
+
return ["db", "cognitive", "doctor", "local_context"];
|
|
1127
1127
|
}
|
|
1128
1128
|
|
|
1129
1129
|
// Brain contracts — files the NEXO Brain publishes to consumers like
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.19.0",
|
|
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/auto_update.py
CHANGED
|
@@ -4350,7 +4350,7 @@ def _restore_runtime_tree(backup_dir: str, dest: Path = NEXO_HOME) -> None:
|
|
|
4350
4350
|
def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_HOME, progress_fn=None) -> dict:
|
|
4351
4351
|
import shutil
|
|
4352
4352
|
|
|
4353
|
-
packages = ["db", "cognitive", "doctor", "dashboard", "rules", "crons", "hooks", "presets"]
|
|
4353
|
+
packages = ["db", "cognitive", "doctor", "local_context", "dashboard", "rules", "crons", "hooks", "presets"]
|
|
4354
4354
|
flat_files = _runtime_flat_files(src_dir)
|
|
4355
4355
|
copied_packages = 0
|
|
4356
4356
|
copied_files = 0
|
|
@@ -5372,6 +5372,11 @@ def startup_preflight(*, entrypoint: str, interactive: bool = False) -> dict:
|
|
|
5372
5372
|
return result
|
|
5373
5373
|
|
|
5374
5374
|
try:
|
|
5375
|
+
env_auto_update = os.environ.get("NEXO_BRAIN_AUTO_UPDATE", "").strip().lower()
|
|
5376
|
+
if env_auto_update in ("0", "false", "off", "no"):
|
|
5377
|
+
result["skipped_reason"] = "auto_update disabled via NEXO_BRAIN_AUTO_UPDATE env var"
|
|
5378
|
+
_write_update_summary(result)
|
|
5379
|
+
return result
|
|
5375
5380
|
last_check = _read_last_check()
|
|
5376
5381
|
now = time.time()
|
|
5377
5382
|
schedule_file = _runtime_config_dir(NEXO_HOME) / "schedule.json"
|
package/src/local_context/api.py
CHANGED
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import shutil
|
|
6
6
|
import stat
|
|
7
7
|
import hashlib
|
|
8
|
+
import subprocess
|
|
8
9
|
import sys
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any
|
|
@@ -15,9 +16,14 @@ from db._schema import run_migrations
|
|
|
15
16
|
from . import embeddings
|
|
16
17
|
from .extractors import chunk_text, entities, extract_text, summarize
|
|
17
18
|
from .logging import log_event, tail
|
|
18
|
-
from .privacy import classify_path, should_extract
|
|
19
|
+
from .privacy import classify_path, should_extract, should_skip_tree
|
|
19
20
|
from .util import content_hash, json_dumps, json_loads, norm_path, now, quick_fingerprint, redact_path, stable_id, system_label, tokenize
|
|
20
21
|
|
|
22
|
+
LOCAL_INDEX_SERVICE_LABEL = "com.nexo.local-index"
|
|
23
|
+
LOCAL_INDEX_SCRIPT_NAME = "nexo-local-index.py"
|
|
24
|
+
LOCAL_INDEX_WINDOWS_TASK = "NEXO Local Memory"
|
|
25
|
+
LOCAL_INDEX_LINUX_UNIT = "nexo-local-index.service"
|
|
26
|
+
|
|
21
27
|
|
|
22
28
|
def ensure_ready() -> None:
|
|
23
29
|
init_db()
|
|
@@ -66,20 +72,62 @@ def list_roots() -> list[dict]:
|
|
|
66
72
|
return [dict(row) for row in rows]
|
|
67
73
|
|
|
68
74
|
|
|
75
|
+
def _dedupe_roots(roots: list[str]) -> list[str]:
|
|
76
|
+
seen: set[str] = set()
|
|
77
|
+
result: list[str] = []
|
|
78
|
+
for root in roots:
|
|
79
|
+
normalized = norm_path(root)
|
|
80
|
+
if not normalized or normalized in seen:
|
|
81
|
+
continue
|
|
82
|
+
seen.add(normalized)
|
|
83
|
+
result.append(normalized)
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _mounted_volume_roots() -> list[str]:
|
|
88
|
+
candidates: list[Path] = []
|
|
89
|
+
if sys.platform == "darwin":
|
|
90
|
+
candidates.extend((Path("/Volumes")).iterdir() if Path("/Volumes").is_dir() else [])
|
|
91
|
+
elif sys.platform.startswith("win"):
|
|
92
|
+
candidates.extend(Path(f"{letter}:\\") for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
93
|
+
else:
|
|
94
|
+
user = os.environ.get("USER") or os.environ.get("USERNAME") or ""
|
|
95
|
+
mount_bases = [Path("/mnt")]
|
|
96
|
+
if user:
|
|
97
|
+
mount_bases.extend([Path("/media") / user, Path("/run/media") / user])
|
|
98
|
+
for base in mount_bases:
|
|
99
|
+
if base.is_dir():
|
|
100
|
+
candidates.extend(base.iterdir())
|
|
101
|
+
|
|
102
|
+
roots: list[str] = []
|
|
103
|
+
root_resolved = Path("/").resolve()
|
|
104
|
+
for candidate in candidates:
|
|
105
|
+
try:
|
|
106
|
+
if candidate.name.startswith(".") or not candidate.is_dir():
|
|
107
|
+
continue
|
|
108
|
+
resolved = candidate.resolve()
|
|
109
|
+
if resolved == root_resolved:
|
|
110
|
+
continue
|
|
111
|
+
roots.append(str(candidate))
|
|
112
|
+
except Exception:
|
|
113
|
+
continue
|
|
114
|
+
return roots
|
|
115
|
+
|
|
116
|
+
|
|
69
117
|
def default_roots() -> list[str]:
|
|
70
118
|
home = Path.home()
|
|
71
119
|
configured = os.environ.get("NEXO_LOCAL_INDEX_DEFAULT_ROOTS", "").strip()
|
|
72
120
|
if configured:
|
|
73
|
-
return [
|
|
74
|
-
return [
|
|
121
|
+
return _dedupe_roots([item for item in configured.split(os.pathsep) if item.strip()])
|
|
122
|
+
return _dedupe_roots([str(home), *_mounted_volume_roots()])
|
|
75
123
|
|
|
76
124
|
|
|
77
125
|
def ensure_default_roots() -> dict:
|
|
78
|
-
|
|
79
|
-
if existing:
|
|
80
|
-
return {"ok": True, "created": 0, "roots": existing}
|
|
126
|
+
existing_paths = {row["root_path"] for row in list_roots()}
|
|
81
127
|
created = []
|
|
82
128
|
for root in default_roots():
|
|
129
|
+
if root in existing_paths:
|
|
130
|
+
continue
|
|
83
131
|
candidate = Path(root).expanduser()
|
|
84
132
|
if candidate.exists() and candidate.is_dir():
|
|
85
133
|
created.append(add_root(str(candidate), mode="normal", depth=2))
|
|
@@ -295,6 +343,8 @@ def _iter_files(root: Path, exclusions: list[str], *, limit: int | None = None,
|
|
|
295
343
|
current = stack.pop()
|
|
296
344
|
if _is_excluded(str(current), exclusions):
|
|
297
345
|
continue
|
|
346
|
+
if current != root and should_skip_tree(str(current)):
|
|
347
|
+
continue
|
|
298
348
|
try:
|
|
299
349
|
st = current.stat()
|
|
300
350
|
except Exception:
|
|
@@ -314,6 +364,8 @@ def _iter_files(root: Path, exclusions: list[str], *, limit: int | None = None,
|
|
|
314
364
|
if entry.is_symlink():
|
|
315
365
|
continue
|
|
316
366
|
if entry.is_dir():
|
|
367
|
+
if should_skip_tree(str(entry)):
|
|
368
|
+
continue
|
|
317
369
|
dirs.append(entry)
|
|
318
370
|
continue
|
|
319
371
|
if entry.is_file():
|
|
@@ -611,6 +663,136 @@ def _problem_rows(conn) -> list[dict]:
|
|
|
611
663
|
]
|
|
612
664
|
|
|
613
665
|
|
|
666
|
+
def _command_output(args: list[str], *, timeout: int = 2) -> tuple[int, str, str]:
|
|
667
|
+
try:
|
|
668
|
+
result = subprocess.run(args, capture_output=True, text=True, timeout=timeout)
|
|
669
|
+
except FileNotFoundError as exc:
|
|
670
|
+
return 127, "", str(exc)
|
|
671
|
+
except subprocess.TimeoutExpired as exc:
|
|
672
|
+
return 124, exc.stdout or "", exc.stderr or "timeout"
|
|
673
|
+
except Exception as exc:
|
|
674
|
+
return 1, "", str(exc)
|
|
675
|
+
return result.returncode, result.stdout or "", result.stderr or ""
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _process_running(pattern: str) -> bool:
|
|
679
|
+
if system_label() == "windows":
|
|
680
|
+
command = (
|
|
681
|
+
"$pattern = '" + pattern.replace("'", "''") + "'; "
|
|
682
|
+
"$match = Get-CimInstance Win32_Process | "
|
|
683
|
+
"Where-Object { $_.CommandLine -like \"*$pattern*\" } | "
|
|
684
|
+
"Select-Object -First 1 -ExpandProperty ProcessId; "
|
|
685
|
+
"if ($match) { Write-Output $match }"
|
|
686
|
+
)
|
|
687
|
+
code, stdout, _ = _command_output(["powershell", "-NoProfile", "-Command", command], timeout=4)
|
|
688
|
+
return code == 0 and bool(stdout.strip())
|
|
689
|
+
|
|
690
|
+
code, stdout, _ = _command_output(["pgrep", "-f", pattern], timeout=2)
|
|
691
|
+
if code == 0 and stdout.strip():
|
|
692
|
+
return True
|
|
693
|
+
code, stdout, _ = _command_output(["ps", "aux"], timeout=2)
|
|
694
|
+
return code == 0 and pattern in stdout
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _macos_local_index_service_status() -> dict:
|
|
698
|
+
plist_path = Path.home() / "Library" / "LaunchAgents" / f"{LOCAL_INDEX_SERVICE_LABEL}.plist"
|
|
699
|
+
installed = plist_path.is_file()
|
|
700
|
+
running = False
|
|
701
|
+
active_process = False
|
|
702
|
+
pid = ""
|
|
703
|
+
|
|
704
|
+
code, stdout, _ = _command_output(["launchctl", "list"], timeout=2)
|
|
705
|
+
if code == 0:
|
|
706
|
+
for line in stdout.splitlines():
|
|
707
|
+
parts = line.split()
|
|
708
|
+
if len(parts) >= 3 and parts[-1] == LOCAL_INDEX_SERVICE_LABEL:
|
|
709
|
+
installed = True
|
|
710
|
+
pid = parts[0]
|
|
711
|
+
running = True
|
|
712
|
+
active_process = pid.isdigit() and int(pid) > 0
|
|
713
|
+
break
|
|
714
|
+
|
|
715
|
+
if not installed:
|
|
716
|
+
code, _, _ = _command_output(["launchctl", "print", f"gui/{os.getuid()}/{LOCAL_INDEX_SERVICE_LABEL}"], timeout=2)
|
|
717
|
+
installed = code == 0
|
|
718
|
+
|
|
719
|
+
if not active_process:
|
|
720
|
+
active_process = _process_running(LOCAL_INDEX_SCRIPT_NAME)
|
|
721
|
+
running = running or active_process
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
"installed": installed,
|
|
725
|
+
"running": running,
|
|
726
|
+
"active_process": active_process,
|
|
727
|
+
"manager": "launchagent",
|
|
728
|
+
"label": LOCAL_INDEX_SERVICE_LABEL,
|
|
729
|
+
"pid": pid,
|
|
730
|
+
"config_path": str(plist_path),
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _windows_local_index_service_status() -> dict:
|
|
735
|
+
command = (
|
|
736
|
+
"$task = Get-ScheduledTask -TaskName 'NEXO Local Memory' -ErrorAction SilentlyContinue; "
|
|
737
|
+
"if ($task) { Write-Output $task.State }"
|
|
738
|
+
)
|
|
739
|
+
code, stdout, _ = _command_output(["powershell", "-NoProfile", "-Command", command], timeout=4)
|
|
740
|
+
task_state = stdout.strip()
|
|
741
|
+
task_state_key = task_state.lower()
|
|
742
|
+
installed = code == 0 and bool(task_state)
|
|
743
|
+
active_process = task_state_key == "running"
|
|
744
|
+
if not active_process:
|
|
745
|
+
active_process = _process_running(LOCAL_INDEX_SCRIPT_NAME)
|
|
746
|
+
running = task_state_key in {"ready", "running"} or active_process
|
|
747
|
+
return {
|
|
748
|
+
"installed": installed,
|
|
749
|
+
"running": running,
|
|
750
|
+
"active_process": active_process,
|
|
751
|
+
"manager": "scheduled_task",
|
|
752
|
+
"task_name": LOCAL_INDEX_WINDOWS_TASK,
|
|
753
|
+
"task_state": task_state,
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _linux_local_index_service_status() -> dict:
|
|
758
|
+
unit_dir = Path.home() / ".config" / "systemd" / "user"
|
|
759
|
+
unit_path = unit_dir / LOCAL_INDEX_LINUX_UNIT
|
|
760
|
+
timer_path = unit_dir / "nexo-local-index.timer"
|
|
761
|
+
installed = unit_path.is_file() or timer_path.is_file()
|
|
762
|
+
|
|
763
|
+
code, stdout, _ = _command_output(["systemctl", "--user", "is-active", LOCAL_INDEX_LINUX_UNIT], timeout=2)
|
|
764
|
+
unit_state = stdout.strip()
|
|
765
|
+
running = code == 0 and unit_state == "active"
|
|
766
|
+
active_process = _process_running(LOCAL_INDEX_SCRIPT_NAME)
|
|
767
|
+
running = running or active_process
|
|
768
|
+
|
|
769
|
+
return {
|
|
770
|
+
"installed": installed,
|
|
771
|
+
"running": running,
|
|
772
|
+
"active_process": active_process,
|
|
773
|
+
"manager": "systemd_user",
|
|
774
|
+
"unit": LOCAL_INDEX_LINUX_UNIT,
|
|
775
|
+
"unit_state": unit_state,
|
|
776
|
+
"config_path": str(unit_path),
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _local_index_service_status() -> dict:
|
|
781
|
+
platform_value = system_label()
|
|
782
|
+
if platform_value == "macos":
|
|
783
|
+
service = _macos_local_index_service_status()
|
|
784
|
+
elif platform_value == "windows":
|
|
785
|
+
service = _windows_local_index_service_status()
|
|
786
|
+
else:
|
|
787
|
+
service = _linux_local_index_service_status()
|
|
788
|
+
service.setdefault("installed", False)
|
|
789
|
+
service.setdefault("running", False)
|
|
790
|
+
service["platform"] = platform_value
|
|
791
|
+
service["started_at"] = ""
|
|
792
|
+
service["last_heartbeat_at"] = ""
|
|
793
|
+
return service
|
|
794
|
+
|
|
795
|
+
|
|
614
796
|
def status() -> dict:
|
|
615
797
|
conn = _conn()
|
|
616
798
|
paused = _is_paused()
|
|
@@ -628,16 +810,11 @@ def status() -> dict:
|
|
|
628
810
|
).fetchall()
|
|
629
811
|
for row in by_volume:
|
|
630
812
|
volumes.append({"id": row["volume_id"], "label": row["volume_id"] or "Disk", "files": row["files"], "status": "active"})
|
|
813
|
+
service = _local_index_service_status()
|
|
814
|
+
service["state"] = "paused" if paused else ("idle" if pending == 0 else "indexing")
|
|
631
815
|
return {
|
|
632
816
|
"ok": True,
|
|
633
|
-
"service":
|
|
634
|
-
"installed": False,
|
|
635
|
-
"running": False,
|
|
636
|
-
"state": "paused" if paused else ("idle" if pending == 0 else "indexing"),
|
|
637
|
-
"platform": system_label(),
|
|
638
|
-
"started_at": "",
|
|
639
|
-
"last_heartbeat_at": "",
|
|
640
|
-
},
|
|
817
|
+
"service": service,
|
|
641
818
|
"global": {
|
|
642
819
|
"phase": "paused" if paused else ("idle" if pending == 0 else "light_extraction"),
|
|
643
820
|
"percent": percent,
|
|
@@ -36,10 +36,24 @@ NOISY_PARTS = {
|
|
|
36
36
|
"dist",
|
|
37
37
|
"build",
|
|
38
38
|
".git",
|
|
39
|
+
".venv",
|
|
40
|
+
"venv",
|
|
41
|
+
"env",
|
|
39
42
|
".cache",
|
|
40
43
|
"cache",
|
|
41
44
|
"coverage",
|
|
42
45
|
"__pycache__",
|
|
46
|
+
".tox",
|
|
47
|
+
".mypy_cache",
|
|
48
|
+
".pytest_cache",
|
|
49
|
+
".ruff_cache",
|
|
50
|
+
".next",
|
|
51
|
+
".nuxt",
|
|
52
|
+
".turbo",
|
|
53
|
+
".parcel-cache",
|
|
54
|
+
".bun",
|
|
55
|
+
".gradle",
|
|
56
|
+
"target",
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
SYSTEM_PARTS = {
|
|
@@ -71,6 +85,15 @@ def classify_path(path: str) -> tuple[int, str, str]:
|
|
|
71
85
|
return 2, "normal", "default"
|
|
72
86
|
|
|
73
87
|
|
|
88
|
+
def should_skip_tree(path: str) -> bool:
|
|
89
|
+
p = Path(path)
|
|
90
|
+
lowered = str(p).replace("\\", "/").lower()
|
|
91
|
+
parts = {part.lower() for part in p.parts}
|
|
92
|
+
if any(item in lowered for item in SYSTEM_PARTS):
|
|
93
|
+
return True
|
|
94
|
+
return bool(parts & NOISY_PARTS)
|
|
95
|
+
|
|
96
|
+
|
|
74
97
|
def should_extract(path: str, depth: int) -> bool:
|
|
75
98
|
if depth < 2:
|
|
76
99
|
return False
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import contextlib
|
|
4
5
|
import hashlib
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
7
8
|
import re
|
|
8
9
|
import shutil
|
|
10
|
+
import sys
|
|
9
11
|
import time
|
|
10
12
|
from dataclasses import dataclass
|
|
11
13
|
from pathlib import Path
|
|
@@ -814,6 +816,31 @@ def prime_process_fingerprint() -> str:
|
|
|
814
816
|
return PROCESS_FINGERPRINT
|
|
815
817
|
|
|
816
818
|
|
|
819
|
+
_DRIFT_AUTOEXIT_SCHEDULED = False
|
|
820
|
+
_DRIFT_EXIT_CODE = 75
|
|
821
|
+
_DRIFT_EXIT_DELAY_SECONDS = 0.5
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _request_drift_exit() -> None:
|
|
825
|
+
try:
|
|
826
|
+
os._exit(_DRIFT_EXIT_CODE)
|
|
827
|
+
except Exception:
|
|
828
|
+
os._exit(1)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _schedule_drift_autoexit() -> None:
|
|
832
|
+
global _DRIFT_AUTOEXIT_SCHEDULED
|
|
833
|
+
if _DRIFT_AUTOEXIT_SCHEDULED:
|
|
834
|
+
return
|
|
835
|
+
_DRIFT_AUTOEXIT_SCHEDULED = True
|
|
836
|
+
try:
|
|
837
|
+
loop = asyncio.get_running_loop()
|
|
838
|
+
except RuntimeError:
|
|
839
|
+
_request_drift_exit()
|
|
840
|
+
return
|
|
841
|
+
loop.call_later(_DRIFT_EXIT_DELAY_SECONDS, _request_drift_exit)
|
|
842
|
+
|
|
843
|
+
|
|
817
844
|
@dataclass
|
|
818
845
|
class RestartRequiredMiddleware(Middleware):
|
|
819
846
|
client: str = ""
|
|
@@ -893,4 +920,7 @@ class RestartRequiredMiddleware(Middleware):
|
|
|
893
920
|
"reason": state["reason"],
|
|
894
921
|
"client_action": state["client_action"],
|
|
895
922
|
}
|
|
896
|
-
|
|
923
|
+
result = await self._tool_result_for_restart_required(context, payload)
|
|
924
|
+
if state.get("reason") == "fingerprint_mismatch":
|
|
925
|
+
_schedule_drift_autoexit()
|
|
926
|
+
return result
|