nexo-brain 7.23.2 → 7.23.4
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 +7 -1
- package/package.json +1 -1
- package/scripts/sync_release_artifacts.py +28 -0
- package/src/auto_update.py +25 -47
- package/src/automation_reconciler.py +383 -0
- package/src/automation_supervisor.py +86 -9
- package/src/backup_retention.py +70 -0
- package/src/cli.py +55 -2
- package/src/cognitive/_core.py +4 -3
- package/src/cognitive_paths.py +194 -0
- package/src/dashboard/app.py +2 -1
- package/src/db/_episodic.py +85 -7
- package/src/db/_schema.py +81 -0
- package/src/db/_skills.py +3 -3
- package/src/disk_recovery/__init__.py +11 -0
- package/src/disk_recovery/handlers/__init__.py +1 -0
- package/src/disk_recovery/handlers/common.py +37 -0
- package/src/disk_recovery/handlers/macos.py +39 -0
- package/src/disk_recovery/handlers/windows.py +49 -0
- package/src/disk_recovery/registry.py +135 -0
- package/src/doctor/providers/boot.py +115 -15
- package/src/kg_populate.py +2 -5
- package/src/paths.py +321 -5
- package/src/plugins/update.py +14 -36
- package/src/pre_answer_router.py +21 -0
- package/src/runtime_service.py +30 -3
- package/src/runtime_versioning.py +272 -10
- package/src/script_registry.py +3 -2
- package/src/scripts/backfill_task_owner.py +10 -4
- package/src/scripts/deep-sleep/apply_findings.py +2 -5
- package/src/scripts/deep-sleep/collect.py +2 -5
- package/src/scripts/nexo-cognitive-decay.py +2 -1
- package/src/scripts/nexo-daily-self-audit.py +36 -10
- package/src/scripts/nexo-followup-runner.py +1 -1
- package/src/scripts/nexo-immune.py +2 -1
- package/src/scripts/nexo-migrate.py +2 -3
- package/src/scripts/post_disk_recovery_sweep.py +75 -0
- package/src/scripts/prune_runtime_backups.py +78 -11
- package/src/server.py +13 -1
- package/src/storage_router.py +2 -3
- package/src/support_snapshot.py +25 -0
- package/src/transcript_index.py +234 -0
- package/src/transcript_utils.py +31 -8
- package/src/user_data_portability.py +2 -3
- package/tool-enforcement-map.json +15 -0
package/src/runtime_service.py
CHANGED
|
@@ -9,6 +9,7 @@ handlers.
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import asyncio
|
|
12
|
+
import hashlib
|
|
12
13
|
import json
|
|
13
14
|
import os
|
|
14
15
|
import signal
|
|
@@ -135,11 +136,22 @@ def read_service_state() -> dict[str, Any]:
|
|
|
135
136
|
return {}
|
|
136
137
|
|
|
137
138
|
|
|
139
|
+
def _new_runtime_instance_id(payload: dict[str, Any]) -> str:
|
|
140
|
+
seed = "|".join(
|
|
141
|
+
str(payload.get(key) or "")
|
|
142
|
+
for key in ("server_path", "runtime_version", "runtime_fingerprint", "started_at", "pid")
|
|
143
|
+
)
|
|
144
|
+
if not seed.strip("|"):
|
|
145
|
+
seed = f"{current_server_path()}|{os.getpid()}|{time.time()}"
|
|
146
|
+
return "rt-" + hashlib.sha256(seed.encode("utf-8")).hexdigest()[:16]
|
|
147
|
+
|
|
148
|
+
|
|
138
149
|
def write_service_state(state: dict[str, Any]) -> None:
|
|
139
150
|
path = service_state_path()
|
|
140
151
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
141
152
|
payload = dict(state)
|
|
142
153
|
payload.update(current_runtime_identity())
|
|
154
|
+
payload["runtime_instance_id"] = str(payload.get("runtime_instance_id") or _new_runtime_instance_id(payload))
|
|
143
155
|
payload["updated_at"] = time.time()
|
|
144
156
|
tmp.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
145
157
|
os.replace(tmp, path)
|
|
@@ -242,17 +254,24 @@ def current_server_path() -> Path:
|
|
|
242
254
|
|
|
243
255
|
def current_runtime_identity() -> dict[str, str]:
|
|
244
256
|
try:
|
|
245
|
-
from runtime_versioning import compute_mcp_runtime_fingerprint, read_version_for_path
|
|
257
|
+
from runtime_versioning import compute_mcp_runtime_fingerprint, read_version_for_path, runtime_generation
|
|
246
258
|
|
|
247
259
|
root = current_server_path().parent
|
|
248
260
|
version = read_version_for_path(root) or read_version_for_path(root.parent)
|
|
261
|
+
fingerprint = compute_mcp_runtime_fingerprint(root, use_cache=True)
|
|
249
262
|
return {
|
|
250
263
|
"runtime_version": version,
|
|
251
|
-
"runtime_fingerprint":
|
|
264
|
+
"runtime_fingerprint": fingerprint,
|
|
265
|
+
"runtime_generation": runtime_generation(version, fingerprint, str(root)),
|
|
252
266
|
"server_path": str(current_server_path()),
|
|
253
267
|
}
|
|
254
268
|
except Exception:
|
|
255
|
-
return {
|
|
269
|
+
return {
|
|
270
|
+
"runtime_version": "",
|
|
271
|
+
"runtime_fingerprint": "",
|
|
272
|
+
"runtime_generation": "unknown",
|
|
273
|
+
"server_path": str(current_server_path()),
|
|
274
|
+
}
|
|
256
275
|
|
|
257
276
|
|
|
258
277
|
def state_matches_current_runtime(state: dict[str, Any]) -> bool:
|
|
@@ -268,6 +287,11 @@ def state_matches_current_runtime(state: dict[str, Any]) -> bool:
|
|
|
268
287
|
if current_fp and state_fp and current_fp != state_fp:
|
|
269
288
|
return False
|
|
270
289
|
|
|
290
|
+
current_generation = str(current.get("runtime_generation") or "").strip()
|
|
291
|
+
state_generation = str(state.get("runtime_generation") or "").strip()
|
|
292
|
+
if current_generation and state_generation and current_generation != state_generation:
|
|
293
|
+
return False
|
|
294
|
+
|
|
271
295
|
current_version = str(current.get("runtime_version") or "").strip()
|
|
272
296
|
state_version = str(state.get("runtime_version") or "").strip()
|
|
273
297
|
if current_version and state_version and current_version != state_version:
|
|
@@ -410,8 +434,11 @@ def runtime_service_status() -> dict[str, Any]:
|
|
|
410
434
|
"stale": bool(state and not state_matches_current_runtime(state)),
|
|
411
435
|
"runtime_version": current.get("runtime_version", ""),
|
|
412
436
|
"runtime_fingerprint": current.get("runtime_fingerprint", ""),
|
|
437
|
+
"runtime_generation": current.get("runtime_generation", "unknown"),
|
|
438
|
+
"runtime_instance_id": str(state.get("runtime_instance_id") or ""),
|
|
413
439
|
"state_runtime_version": str(state.get("runtime_version") or ""),
|
|
414
440
|
"state_runtime_fingerprint": str(state.get("runtime_fingerprint") or ""),
|
|
441
|
+
"state_runtime_generation": str(state.get("runtime_generation") or ""),
|
|
415
442
|
"state_path": str(service_state_path()),
|
|
416
443
|
"log_path": str(service_log_path()),
|
|
417
444
|
"server_path": str(current_server_path()),
|
|
@@ -19,7 +19,8 @@ import paths
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
CONTINUITY_API_LEVEL = 1
|
|
22
|
-
MCP_STATUS_SCHEMA_VERSION =
|
|
22
|
+
MCP_STATUS_SCHEMA_VERSION = 3
|
|
23
|
+
CLIENT_STATE_SCHEMA_VERSION = 1
|
|
23
24
|
PROCESS_VERSION = ""
|
|
24
25
|
PROCESS_FINGERPRINT = ""
|
|
25
26
|
|
|
@@ -61,11 +62,11 @@ RESTART_ALLOWLIST = {
|
|
|
61
62
|
"nexo_continuity_snapshot_read",
|
|
62
63
|
"nexo_continuity_resume_bundle",
|
|
63
64
|
"nexo_continuity_audit",
|
|
64
|
-
# v0.32.5 —
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
65
|
+
# v0.32.5 — read-only tools called by the CORE protocol immediately after
|
|
66
|
+
# `nexo_startup` (memory recall, reminders, followups, context, doctor).
|
|
67
|
+
# Without this allowlist they were blocked by mcp_restart_required after
|
|
68
|
+
# `nexo update` while a session was active, making continuity appear lost
|
|
69
|
+
# until the client was closed and reopened.
|
|
69
70
|
"nexo_smart_startup",
|
|
70
71
|
"nexo_session_diary_read",
|
|
71
72
|
"nexo_session_diary_write",
|
|
@@ -178,6 +179,135 @@ def restart_required_marker_path() -> Path:
|
|
|
178
179
|
return paths.operations_dir() / "mcp-restart-required.json"
|
|
179
180
|
|
|
180
181
|
|
|
182
|
+
def mcp_client_state_path() -> Path:
|
|
183
|
+
return paths.runtime_state_dir() / "mcp-client-state.json"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def runtime_generation(version: str = "", fingerprint: str = "", root: str = "") -> str:
|
|
187
|
+
seed = "|".join(part for part in (version, fingerprint, root) if str(part or "").strip())
|
|
188
|
+
if not seed:
|
|
189
|
+
return "unknown"
|
|
190
|
+
return hashlib.sha256(seed.encode("utf-8")).hexdigest()[:16]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _read_mcp_client_state_file() -> dict:
|
|
194
|
+
path = mcp_client_state_path()
|
|
195
|
+
if not path.is_file():
|
|
196
|
+
return {
|
|
197
|
+
"schema_version": CLIENT_STATE_SCHEMA_VERSION,
|
|
198
|
+
"clients": {},
|
|
199
|
+
}
|
|
200
|
+
try:
|
|
201
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
202
|
+
except Exception:
|
|
203
|
+
return {
|
|
204
|
+
"schema_version": CLIENT_STATE_SCHEMA_VERSION,
|
|
205
|
+
"clients": {},
|
|
206
|
+
"corrupt": True,
|
|
207
|
+
}
|
|
208
|
+
if not isinstance(payload, dict):
|
|
209
|
+
return {
|
|
210
|
+
"schema_version": CLIENT_STATE_SCHEMA_VERSION,
|
|
211
|
+
"clients": {},
|
|
212
|
+
"corrupt": True,
|
|
213
|
+
}
|
|
214
|
+
clients = payload.get("clients")
|
|
215
|
+
if not isinstance(clients, dict):
|
|
216
|
+
payload["clients"] = {}
|
|
217
|
+
payload.setdefault("schema_version", CLIENT_STATE_SCHEMA_VERSION)
|
|
218
|
+
return payload
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _write_mcp_client_state_file(payload: dict) -> None:
|
|
222
|
+
payload = dict(payload)
|
|
223
|
+
payload["schema_version"] = CLIENT_STATE_SCHEMA_VERSION
|
|
224
|
+
payload["updated_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
225
|
+
_write_json_atomic(mcp_client_state_path(), payload)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def read_mcp_client_states() -> dict:
|
|
229
|
+
"""Return the persisted per-client MCP readiness registry."""
|
|
230
|
+
payload = _read_mcp_client_state_file()
|
|
231
|
+
clients = payload.get("clients")
|
|
232
|
+
if not isinstance(clients, dict):
|
|
233
|
+
clients = {}
|
|
234
|
+
return {
|
|
235
|
+
"schema_version": CLIENT_STATE_SCHEMA_VERSION,
|
|
236
|
+
"path": str(mcp_client_state_path()),
|
|
237
|
+
"clients": clients,
|
|
238
|
+
"corrupt": bool(payload.get("corrupt")),
|
|
239
|
+
"updated_at": str(payload.get("updated_at") or ""),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _probe_reason_code(probe: dict) -> str:
|
|
244
|
+
if not probe.get("probe_ok", probe.get("ok", False)):
|
|
245
|
+
return str(probe.get("error") or "mcp_probe_failed")
|
|
246
|
+
try:
|
|
247
|
+
tool_count = int(probe.get("tool_count") or 0)
|
|
248
|
+
except Exception:
|
|
249
|
+
tool_count = 0
|
|
250
|
+
if tool_count <= 0:
|
|
251
|
+
return "tools_missing"
|
|
252
|
+
missing = probe.get("missing_required_tools")
|
|
253
|
+
has_required_tools_contract = isinstance(
|
|
254
|
+
probe.get("required_tools_present"), bool
|
|
255
|
+
) or isinstance(missing, list)
|
|
256
|
+
if not has_required_tools_contract:
|
|
257
|
+
return "required_tools_contract_missing"
|
|
258
|
+
if probe.get("required_tools_present") is False:
|
|
259
|
+
return "required_tools_missing"
|
|
260
|
+
if isinstance(missing, list) and missing:
|
|
261
|
+
return "required_tools_missing"
|
|
262
|
+
return "ready"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def record_mcp_client_probe(*, client: str = "", probe: dict | None = None) -> dict:
|
|
266
|
+
"""Persist the latest probe result for one MCP client."""
|
|
267
|
+
normalized = _normalize_restart_client(client or (probe or {}).get("client", ""))
|
|
268
|
+
if not normalized:
|
|
269
|
+
return {"ok": False, "error": "unknown_client"}
|
|
270
|
+
probe = dict(probe or {})
|
|
271
|
+
installed_version_value = str(probe.get("installed_version") or installed_runtime_version() or "").strip()
|
|
272
|
+
installed_fp = str(probe.get("installed_fingerprint") or installed_runtime_fingerprint() or "").strip()
|
|
273
|
+
root = str(active_runtime_root())
|
|
274
|
+
generation = str(probe.get("runtime_generation") or runtime_generation(installed_version_value, installed_fp, root))
|
|
275
|
+
reason_code = _probe_reason_code(probe)
|
|
276
|
+
probe_ok = reason_code == "ready"
|
|
277
|
+
try:
|
|
278
|
+
tool_count = int(probe.get("tool_count") or 0)
|
|
279
|
+
except Exception:
|
|
280
|
+
tool_count = 0
|
|
281
|
+
missing_required = probe.get("missing_required_tools")
|
|
282
|
+
if not isinstance(missing_required, list):
|
|
283
|
+
missing_required = []
|
|
284
|
+
required_tools_present_raw = probe.get("required_tools_present")
|
|
285
|
+
required_tools_present = (
|
|
286
|
+
required_tools_present_raw
|
|
287
|
+
if isinstance(required_tools_present_raw, bool)
|
|
288
|
+
else False
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
payload = _read_mcp_client_state_file()
|
|
292
|
+
clients = dict(payload.get("clients") or {})
|
|
293
|
+
row = {
|
|
294
|
+
"client": normalized,
|
|
295
|
+
"last_seen_generation": generation,
|
|
296
|
+
"last_tool_count": tool_count,
|
|
297
|
+
"last_probe_ok": probe_ok,
|
|
298
|
+
"last_fingerprint": installed_fp,
|
|
299
|
+
"last_probe_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
300
|
+
"required_tools_present": required_tools_present,
|
|
301
|
+
"missing_required_tools": missing_required,
|
|
302
|
+
"reason_code": reason_code,
|
|
303
|
+
"client_action": "ready" if probe_ok else "reprobe",
|
|
304
|
+
}
|
|
305
|
+
clients[normalized] = row
|
|
306
|
+
payload["clients"] = clients
|
|
307
|
+
_write_mcp_client_state_file(payload)
|
|
308
|
+
return {"ok": True, **row}
|
|
309
|
+
|
|
310
|
+
|
|
181
311
|
def fingerprint_cache_path() -> Path:
|
|
182
312
|
"""Where the runtime fingerprint cache lives.
|
|
183
313
|
|
|
@@ -634,8 +764,9 @@ def clear_restart_required_marker(
|
|
|
634
764
|
pending_clients = {k: v for k, v in clients.items() if v != "ok"}
|
|
635
765
|
effective_installed = str(installed_version or payload.get("to_version") or "").strip()
|
|
636
766
|
effective_process = str(process_version or "").strip()
|
|
767
|
+
marker_to_fingerprint = str(payload.get("to_fingerprint") or "").strip()
|
|
637
768
|
effective_installed_fp = str(
|
|
638
|
-
installed_fingerprint or
|
|
769
|
+
installed_fingerprint or marker_to_fingerprint or ""
|
|
639
770
|
).strip()
|
|
640
771
|
effective_process_fp = str(
|
|
641
772
|
process_fingerprint or PROCESS_FINGERPRINT or ""
|
|
@@ -643,8 +774,9 @@ def clear_restart_required_marker(
|
|
|
643
774
|
if pending_clients:
|
|
644
775
|
_write_json_atomic(path, payload)
|
|
645
776
|
return {"ok": True, "cleared": False, "path": str(path), "pending_clients": pending_clients}
|
|
646
|
-
# Prefer fingerprint match when both sides have it
|
|
647
|
-
#
|
|
777
|
+
# Prefer fingerprint match when both sides have it. For markers that were
|
|
778
|
+
# created with a target fingerprint, do not fall back to version-only
|
|
779
|
+
# clearing: matching versions can still be a stale in-place source update.
|
|
648
780
|
if (
|
|
649
781
|
effective_installed_fp
|
|
650
782
|
and effective_process_fp
|
|
@@ -658,6 +790,14 @@ def clear_restart_required_marker(
|
|
|
658
790
|
"path": str(path),
|
|
659
791
|
"pending_reason": "process_fingerprint_mismatch",
|
|
660
792
|
}
|
|
793
|
+
elif marker_to_fingerprint:
|
|
794
|
+
_write_json_atomic(path, payload)
|
|
795
|
+
return {
|
|
796
|
+
"ok": True,
|
|
797
|
+
"cleared": False,
|
|
798
|
+
"path": str(path),
|
|
799
|
+
"pending_reason": "process_fingerprint_missing",
|
|
800
|
+
}
|
|
661
801
|
elif effective_installed and effective_process and effective_installed != effective_process:
|
|
662
802
|
_write_json_atomic(path, payload)
|
|
663
803
|
return {
|
|
@@ -687,6 +827,7 @@ def resolve_restart_required(
|
|
|
687
827
|
process = str(process_version or PROCESS_VERSION or installed).strip()
|
|
688
828
|
installed_fp = str(installed_fingerprint or installed_runtime_fingerprint() or "").strip()
|
|
689
829
|
process_fp = str(process_fingerprint or PROCESS_FINGERPRINT or "").strip()
|
|
830
|
+
marker_fp = str(marker.get("to_fingerprint") or "").strip()
|
|
690
831
|
restart_required = False
|
|
691
832
|
reason = ""
|
|
692
833
|
client_action = ""
|
|
@@ -706,6 +847,9 @@ def resolve_restart_required(
|
|
|
706
847
|
# fingerprint change and therefore never reach this branch.
|
|
707
848
|
restart_required = True
|
|
708
849
|
reason = reason or "fingerprint_mismatch"
|
|
850
|
+
elif marker.get("required") and marker_fp and (not process_fp or process_fp == "unknown"):
|
|
851
|
+
restart_required = True
|
|
852
|
+
reason = reason or "process_fingerprint_missing"
|
|
709
853
|
elif not fingerprint_usable and installed and process and installed != process:
|
|
710
854
|
# Fallback: when fingerprint can't be computed (missing source tree,
|
|
711
855
|
# unreadable files, fresh install), fall back to the legacy version
|
|
@@ -728,6 +872,105 @@ def resolve_restart_required(
|
|
|
728
872
|
}
|
|
729
873
|
|
|
730
874
|
|
|
875
|
+
def _mcp_client_readiness(
|
|
876
|
+
*,
|
|
877
|
+
client: str,
|
|
878
|
+
state: dict,
|
|
879
|
+
installed_version_value: str,
|
|
880
|
+
installed_fp: str,
|
|
881
|
+
service_status: dict,
|
|
882
|
+
) -> dict:
|
|
883
|
+
generation = runtime_generation(installed_version_value, installed_fp, str(active_runtime_root()))
|
|
884
|
+
process_fp = str(state.get("process_fingerprint") or "").strip()
|
|
885
|
+
service_ok = bool(service_status.get("ok", True))
|
|
886
|
+
fingerprint_ready = (
|
|
887
|
+
bool(installed_fp)
|
|
888
|
+
and bool(process_fp)
|
|
889
|
+
and process_fp != "unknown"
|
|
890
|
+
and installed_fp == process_fp
|
|
891
|
+
)
|
|
892
|
+
global_ready = (
|
|
893
|
+
not bool(state.get("restart_required"))
|
|
894
|
+
and fingerprint_ready
|
|
895
|
+
and service_ok
|
|
896
|
+
)
|
|
897
|
+
if not service_ok:
|
|
898
|
+
global_reason = "runtime_service_unavailable"
|
|
899
|
+
elif not installed_fp:
|
|
900
|
+
global_reason = "installed_fingerprint_missing"
|
|
901
|
+
elif not process_fp or process_fp == "unknown":
|
|
902
|
+
global_reason = "process_fingerprint_missing"
|
|
903
|
+
elif installed_fp != process_fp:
|
|
904
|
+
global_reason = "process_fingerprint_mismatch"
|
|
905
|
+
else:
|
|
906
|
+
global_reason = "ready"
|
|
907
|
+
if state.get("restart_required"):
|
|
908
|
+
return {
|
|
909
|
+
"runtime_generation": generation,
|
|
910
|
+
"global_ready": False,
|
|
911
|
+
"client_ready": False,
|
|
912
|
+
"reason_code": str(state.get("reason") or "restart_required"),
|
|
913
|
+
"client_action": str(state.get("client_action") or "restart_client"),
|
|
914
|
+
"client_state": {},
|
|
915
|
+
}
|
|
916
|
+
if not client:
|
|
917
|
+
return {
|
|
918
|
+
"runtime_generation": generation,
|
|
919
|
+
"global_ready": global_ready,
|
|
920
|
+
"client_ready": global_ready,
|
|
921
|
+
"reason_code": "ready" if global_ready else global_reason,
|
|
922
|
+
"client_action": "ready" if global_ready else "reprobe",
|
|
923
|
+
"client_state": {},
|
|
924
|
+
}
|
|
925
|
+
if not global_ready:
|
|
926
|
+
return {
|
|
927
|
+
"runtime_generation": generation,
|
|
928
|
+
"global_ready": False,
|
|
929
|
+
"client_ready": False,
|
|
930
|
+
"reason_code": global_reason,
|
|
931
|
+
"client_action": "reprobe",
|
|
932
|
+
"client_state": {},
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
registry = read_mcp_client_states()
|
|
936
|
+
client_state = dict((registry.get("clients") or {}).get(client) or {})
|
|
937
|
+
if not client_state:
|
|
938
|
+
return {
|
|
939
|
+
"runtime_generation": generation,
|
|
940
|
+
"global_ready": global_ready,
|
|
941
|
+
"client_ready": False,
|
|
942
|
+
"reason_code": "client_probe_missing",
|
|
943
|
+
"client_action": "reprobe",
|
|
944
|
+
"client_state": {},
|
|
945
|
+
}
|
|
946
|
+
if str(client_state.get("last_seen_generation") or "") != generation:
|
|
947
|
+
return {
|
|
948
|
+
"runtime_generation": generation,
|
|
949
|
+
"global_ready": global_ready,
|
|
950
|
+
"client_ready": False,
|
|
951
|
+
"reason_code": "client_generation_stale",
|
|
952
|
+
"client_action": "reprobe",
|
|
953
|
+
"client_state": client_state,
|
|
954
|
+
}
|
|
955
|
+
if not client_state.get("last_probe_ok"):
|
|
956
|
+
return {
|
|
957
|
+
"runtime_generation": generation,
|
|
958
|
+
"global_ready": global_ready,
|
|
959
|
+
"client_ready": False,
|
|
960
|
+
"reason_code": str(client_state.get("reason_code") or "client_probe_failed"),
|
|
961
|
+
"client_action": str(client_state.get("client_action") or "reprobe"),
|
|
962
|
+
"client_state": client_state,
|
|
963
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
"runtime_generation": generation,
|
|
966
|
+
"global_ready": global_ready,
|
|
967
|
+
"client_ready": bool(global_ready),
|
|
968
|
+
"reason_code": "ready" if global_ready else global_reason,
|
|
969
|
+
"client_action": "ready" if global_ready else "reprobe",
|
|
970
|
+
"client_state": client_state,
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
|
|
731
974
|
def build_mcp_status(*, client: str = "") -> dict:
|
|
732
975
|
client = _normalize_restart_client(client)
|
|
733
976
|
state = resolve_restart_required(client=client)
|
|
@@ -744,6 +987,14 @@ def build_mcp_status(*, client: str = "") -> dict:
|
|
|
744
987
|
"error": "runtime_service_status_unavailable",
|
|
745
988
|
"message": str(exc)[:300],
|
|
746
989
|
}
|
|
990
|
+
readiness = _mcp_client_readiness(
|
|
991
|
+
client=client,
|
|
992
|
+
state=state,
|
|
993
|
+
installed_version_value=state["installed_version"],
|
|
994
|
+
installed_fp=installed_fp,
|
|
995
|
+
service_status=service_status,
|
|
996
|
+
)
|
|
997
|
+
client_states = read_mcp_client_states()
|
|
747
998
|
return {
|
|
748
999
|
"ok": True,
|
|
749
1000
|
"schema_version": MCP_STATUS_SCHEMA_VERSION,
|
|
@@ -762,7 +1013,18 @@ def build_mcp_status(*, client: str = "") -> dict:
|
|
|
762
1013
|
"active_runtime_version": read_version_for_path(active_runtime_root()),
|
|
763
1014
|
"restart_required": bool(state["restart_required"]),
|
|
764
1015
|
"reason": state["reason"],
|
|
765
|
-
"client_action":
|
|
1016
|
+
"client_action": readiness["client_action"],
|
|
1017
|
+
"reason_code": readiness["reason_code"],
|
|
1018
|
+
"global_ready": bool(readiness["global_ready"]),
|
|
1019
|
+
"client_ready": bool(readiness["client_ready"]),
|
|
1020
|
+
"runtime_generation": readiness["runtime_generation"],
|
|
1021
|
+
"client_state": readiness["client_state"],
|
|
1022
|
+
"last_seen_generation": readiness["client_state"].get("last_seen_generation", ""),
|
|
1023
|
+
"last_tool_count": readiness["client_state"].get("last_tool_count", 0),
|
|
1024
|
+
"last_probe_ok": readiness["client_state"].get("last_probe_ok", False),
|
|
1025
|
+
"last_fingerprint": readiness["client_state"].get("last_fingerprint", ""),
|
|
1026
|
+
"client_states": client_states.get("clients", {}),
|
|
1027
|
+
"client_state_path": client_states.get("path", str(mcp_client_state_path())),
|
|
766
1028
|
"marker_path": marker.get("path", str(restart_required_marker_path())),
|
|
767
1029
|
"marker_exists": bool(marker.get("exists")),
|
|
768
1030
|
"marker_corrupt": bool(marker.get("corrupt")),
|
package/src/script_registry.py
CHANGED
|
@@ -1886,8 +1886,7 @@ def retire_superseded_personal_scripts(*, dry_run: bool = False) -> dict:
|
|
|
1886
1886
|
report["unscheduled"].append(removed)
|
|
1887
1887
|
|
|
1888
1888
|
if backup_root is None:
|
|
1889
|
-
backup_root = paths.
|
|
1890
|
-
backup_root.mkdir(parents=True, exist_ok=True)
|
|
1889
|
+
backup_root = paths.create_backup_dir("retired-personal-scripts")
|
|
1891
1890
|
target = backup_root / path.name
|
|
1892
1891
|
suffix = 2
|
|
1893
1892
|
while target.exists():
|
|
@@ -1902,6 +1901,8 @@ def retire_superseded_personal_scripts(*, dry_run: bool = False) -> dict:
|
|
|
1902
1901
|
})
|
|
1903
1902
|
except Exception as exc:
|
|
1904
1903
|
report["errors"].append({"path": str(path), "error": str(exc)})
|
|
1904
|
+
if backup_root is not None:
|
|
1905
|
+
paths.finalize_backup_snapshot(backup_root)
|
|
1905
1906
|
return report
|
|
1906
1907
|
|
|
1907
1908
|
|
|
@@ -40,6 +40,12 @@ import sys
|
|
|
40
40
|
import time
|
|
41
41
|
from pathlib import Path
|
|
42
42
|
|
|
43
|
+
SOURCE_ROOT = Path(__file__).resolve().parents[1]
|
|
44
|
+
if str(SOURCE_ROOT) not in sys.path:
|
|
45
|
+
sys.path.insert(0, str(SOURCE_ROOT))
|
|
46
|
+
|
|
47
|
+
import paths
|
|
48
|
+
|
|
43
49
|
|
|
44
50
|
DEFAULT_DB_PATH = Path.home() / ".nexo" / "runtime" / "data" / "nexo.db"
|
|
45
51
|
DEFAULT_CALIBRATION = Path.home() / ".nexo" / "brain" / "calibration.json"
|
|
@@ -253,11 +259,11 @@ def _has_column(conn: sqlite3.Connection, table: str, col: str) -> bool:
|
|
|
253
259
|
|
|
254
260
|
|
|
255
261
|
def _backup_db(db_path: Path) -> Path:
|
|
256
|
-
|
|
257
|
-
backup =
|
|
258
|
-
backup.parent.mkdir(parents=True, exist_ok=True)
|
|
262
|
+
backup_dir = paths.create_backup_dir("pre-backfill-owner")
|
|
263
|
+
backup = backup_dir / db_path.name
|
|
259
264
|
shutil.copy2(db_path, backup)
|
|
260
|
-
_rotate_backup_family(
|
|
265
|
+
_rotate_backup_family(backup_dir.parent)
|
|
266
|
+
paths.finalize_backup_snapshot(backup_dir)
|
|
261
267
|
return backup
|
|
262
268
|
|
|
263
269
|
|
|
@@ -32,6 +32,7 @@ if str(NEXO_CODE) not in sys.path:
|
|
|
32
32
|
|
|
33
33
|
import db as nexo_db
|
|
34
34
|
import paths
|
|
35
|
+
from cognitive_paths import resolve_cognitive_db
|
|
35
36
|
|
|
36
37
|
def _resolve_nexo_db() -> Path:
|
|
37
38
|
candidates = [
|
|
@@ -53,11 +54,7 @@ def _resolve_nexo_db() -> Path:
|
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
def _resolve_cognitive_db() -> Path:
|
|
56
|
-
|
|
57
|
-
legacy = paths.legacy_data_dir() / "cognitive.db"
|
|
58
|
-
if not new.is_file() and legacy.is_file():
|
|
59
|
-
return legacy
|
|
60
|
-
return new
|
|
57
|
+
return resolve_cognitive_db(for_write=True)
|
|
61
58
|
|
|
62
59
|
|
|
63
60
|
def _resolve_operations_dir() -> Path:
|
|
@@ -27,6 +27,7 @@ if str(NEXO_CODE) not in sys.path:
|
|
|
27
27
|
|
|
28
28
|
import transcript_utils as _transcripts
|
|
29
29
|
import paths
|
|
30
|
+
from cognitive_paths import resolve_cognitive_db
|
|
30
31
|
|
|
31
32
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
32
33
|
|
|
@@ -51,11 +52,7 @@ def _resolve_nexo_db() -> Path:
|
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
def _resolve_cognitive_db() -> Path:
|
|
54
|
-
|
|
55
|
-
legacy = paths.legacy_data_dir() / "cognitive.db"
|
|
56
|
-
if not new.is_file() and legacy.is_file():
|
|
57
|
-
return legacy
|
|
58
|
-
return new
|
|
55
|
+
return resolve_cognitive_db(for_write=False)
|
|
59
56
|
|
|
60
57
|
|
|
61
58
|
def _resolve_deep_sleep_dir() -> Path:
|
|
@@ -37,6 +37,7 @@ _repo_src = _script_dir.parent # src/scripts/ -> src/
|
|
|
37
37
|
NEXO_CODE = _bootstrap_nexo_code(_repo_src)
|
|
38
38
|
from datetime import datetime, timedelta
|
|
39
39
|
|
|
40
|
+
from cognitive_paths import resolve_cognitive_db
|
|
40
41
|
from paths import data_dir, operations_dir
|
|
41
42
|
import cognitive
|
|
42
43
|
|
|
@@ -87,7 +88,7 @@ def _open_correction_fatigue_followup(fatigued: list) -> str:
|
|
|
87
88
|
lines.append(f"... and {len(fatigued) - 10} more")
|
|
88
89
|
description = "\n".join(lines)
|
|
89
90
|
verification = (
|
|
90
|
-
f"sqlite3 {str(
|
|
91
|
+
f"sqlite3 {str(resolve_cognitive_db(for_write=False))} \"SELECT id, content, strength, tags "
|
|
91
92
|
"FROM ltm_memories WHERE tags LIKE '%under_review%' ORDER BY strength ASC LIMIT 50\""
|
|
92
93
|
)
|
|
93
94
|
now_epoch = datetime.now().timestamp()
|
|
@@ -73,6 +73,7 @@ from paths import (
|
|
|
73
73
|
from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
|
|
74
74
|
from constants import AUTOMATION_SUBPROCESS_TIMEOUT
|
|
75
75
|
from core_prompts import render_core_prompt
|
|
76
|
+
from cognitive_paths import audit_cognitive_db_paths, resolve_cognitive_db
|
|
76
77
|
import db as nexo_db
|
|
77
78
|
from public_evolution_queue import queue_public_port_candidate
|
|
78
79
|
|
|
@@ -914,15 +915,34 @@ def check_evolution_health():
|
|
|
914
915
|
|
|
915
916
|
|
|
916
917
|
def check_disk_space():
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
918
|
+
import paths as paths_module
|
|
919
|
+
|
|
920
|
+
try:
|
|
921
|
+
floor = int(paths_module.backup_min_free_bytes())
|
|
922
|
+
usage_before = shutil.disk_usage(str(paths_module.home()))
|
|
923
|
+
if usage_before.free < floor:
|
|
924
|
+
paths_module.aggressive_runtime_backup_prune(
|
|
925
|
+
min_free_bytes=floor,
|
|
926
|
+
reason="daily_self_audit_disk_space",
|
|
927
|
+
)
|
|
928
|
+
usage = shutil.disk_usage(str(paths_module.home()))
|
|
929
|
+
if usage_before.free < floor <= usage.free:
|
|
930
|
+
script = NEXO_CODE / "scripts" / "post_disk_recovery_sweep.py"
|
|
931
|
+
if script.is_file():
|
|
932
|
+
subprocess.run(
|
|
933
|
+
[sys.executable, str(script), "--reason", "daily_self_audit_disk_low_to_ok"],
|
|
934
|
+
capture_output=True,
|
|
935
|
+
text=True,
|
|
936
|
+
timeout=60,
|
|
937
|
+
)
|
|
938
|
+
usage_pct = int(((usage.total - usage.free) / usage.total) * 100)
|
|
939
|
+
free_gb = usage.free / (1024 ** 3)
|
|
940
|
+
if usage.free < floor:
|
|
941
|
+
finding("ERROR", "disk", f"Root disk at {usage_pct}% capacity after NEXO self-cleanup ({free_gb:.1f} GB free)")
|
|
942
|
+
elif usage_pct > 80:
|
|
943
|
+
finding("WARN", "disk", f"Root disk at {usage_pct}% capacity ({free_gb:.1f} GB free)")
|
|
944
|
+
except Exception as exc:
|
|
945
|
+
finding("WARN", "disk", f"Could not check disk space: {exc}")
|
|
926
946
|
|
|
927
947
|
|
|
928
948
|
def check_db_size():
|
|
@@ -1735,7 +1755,13 @@ def check_watchdog_smoke():
|
|
|
1735
1755
|
|
|
1736
1756
|
|
|
1737
1757
|
def check_cognitive_health():
|
|
1738
|
-
|
|
1758
|
+
path_audit = audit_cognitive_db_paths()
|
|
1759
|
+
if path_audit["status"] == "error":
|
|
1760
|
+
finding("ERROR", "cognitive-paths", path_audit["reason"])
|
|
1761
|
+
return
|
|
1762
|
+
if path_audit["status"] == "warning":
|
|
1763
|
+
finding("WARN", "cognitive-paths", path_audit["reason"])
|
|
1764
|
+
cognitive_db = resolve_cognitive_db(for_write=False)
|
|
1739
1765
|
if not cognitive_db.exists():
|
|
1740
1766
|
finding("WARN", "cognitive", "cognitive.db not found")
|
|
1741
1767
|
return
|
|
@@ -301,7 +301,7 @@ def get_all_active_followups(state: dict) -> dict:
|
|
|
301
301
|
rows = conn.execute(
|
|
302
302
|
"SELECT id, description, date, reasoning, verification, priority, recurrence, status, owner, updated_at "
|
|
303
303
|
"FROM followups WHERE status NOT LIKE 'COMPLETED%' "
|
|
304
|
-
"AND UPPER(COALESCE(status, '')) NOT IN ('BLOCKED', 'ARCHIVED', 'DELETED', 'WAITING') "
|
|
304
|
+
"AND UPPER(COALESCE(status, '')) NOT IN ('BLOCKED', 'ARCHIVED', 'DELETED', 'WAITING', 'DONE') "
|
|
305
305
|
"AND description NOT LIKE '[Abandoned]%' "
|
|
306
306
|
"ORDER BY "
|
|
307
307
|
" CASE priority "
|
|
@@ -40,6 +40,7 @@ if str(NEXO_CODE) not in sys.path:
|
|
|
40
40
|
from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
|
|
41
41
|
from constants import AUTOMATION_SUBPROCESS_TIMEOUT
|
|
42
42
|
from core_prompts import render_core_prompt
|
|
43
|
+
from cognitive_paths import resolve_cognitive_db
|
|
43
44
|
import paths
|
|
44
45
|
|
|
45
46
|
from urllib.request import Request, urlopen
|
|
@@ -382,7 +383,7 @@ def check_databases():
|
|
|
382
383
|
|
|
383
384
|
dbs = [
|
|
384
385
|
("nexo.db", paths.db_path()),
|
|
385
|
-
("cognitive.db",
|
|
386
|
+
("cognitive.db", resolve_cognitive_db(for_write=False)),
|
|
386
387
|
]
|
|
387
388
|
if CLAUDE_MEM_DB.exists():
|
|
388
389
|
dbs.append(("claude-mem.db", CLAUDE_MEM_DB))
|
|
@@ -103,9 +103,7 @@ def get_target_version() -> str:
|
|
|
103
103
|
|
|
104
104
|
def backup_databases() -> str:
|
|
105
105
|
"""Backup all .db files before migration. Returns backup dir path."""
|
|
106
|
-
|
|
107
|
-
backup_dir = paths.backups_dir() / f"pre-migrate-{ts}"
|
|
108
|
-
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
backup_dir = paths.create_backup_dir("pre-migrate")
|
|
109
107
|
|
|
110
108
|
data_dir = paths.data_dir()
|
|
111
109
|
if data_dir.exists():
|
|
@@ -123,6 +121,7 @@ def backup_databases() -> str:
|
|
|
123
121
|
if vfile.exists():
|
|
124
122
|
shutil.copy2(vfile, backup_dir / "version.json")
|
|
125
123
|
|
|
124
|
+
paths.finalize_backup_snapshot(backup_dir)
|
|
126
125
|
return str(backup_dir)
|
|
127
126
|
|
|
128
127
|
|