nexo-brain 7.30.5 → 7.30.7
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/package.json +1 -1
- package/src/auto_update.py +15 -0
- package/src/deep_sleep_retention.py +320 -0
- package/src/scripts/deep-sleep/retention.py +15 -0
- package/src/scripts/nexo-deep-sleep.sh +11 -0
- package/src/scripts/nexo-sleep.py +14 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.7",
|
|
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,9 @@
|
|
|
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.30.
|
|
21
|
+
Version `7.30.7` is the current packaged-runtime line. Patch release over v7.30.6 - the Deep Sleep retention update is republished with the required release smoke contract so final closeout, npm, GitHub, and runtime verification stay aligned.
|
|
22
|
+
|
|
23
|
+
Previously in `7.30.6`: patch release over v7.30.5 - Deep Sleep now rotates its operational artifacts and logs automatically, keeping historical installs bounded without touching local-context memory.
|
|
22
24
|
|
|
23
25
|
Previously in `7.30.4`: patch release over v7.30.3 - local runtime update post-sync now gives bounded Memory Fabric repair enough time to finish, and headless automations now treat `nexo_stop` as a terminal close so followup/deep-sleep runners do not reopen no-op protocol loops.
|
|
24
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.7",
|
|
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
|
@@ -2275,6 +2275,21 @@ def _heal_deep_sleep_runtime(dest: Path = NEXO_HOME) -> list[str]:
|
|
|
2275
2275
|
except OSError:
|
|
2276
2276
|
pass
|
|
2277
2277
|
|
|
2278
|
+
# (5) Bound Deep Sleep operational artifacts for existing installs. Older
|
|
2279
|
+
# versions accumulated DB backups and large context dumps indefinitely; this
|
|
2280
|
+
# apply path runs on update so users do not have to wait for the next night.
|
|
2281
|
+
try:
|
|
2282
|
+
from deep_sleep_retention import prune_deep_sleep_runtime
|
|
2283
|
+
|
|
2284
|
+
report = prune_deep_sleep_runtime(nexo_home=dest, apply=True)
|
|
2285
|
+
deleted = int(report.get("deleted_count") or 0)
|
|
2286
|
+
rotated = int(report.get("logs_rotated") or 0)
|
|
2287
|
+
freed = int(report.get("deleted_bytes") or 0) + int(report.get("log_bytes_trimmed") or 0)
|
|
2288
|
+
if deleted or rotated:
|
|
2289
|
+
actions.append(f"retention:{deleted}-deleted:{rotated}-logs:{freed}-bytes")
|
|
2290
|
+
except Exception as exc:
|
|
2291
|
+
actions.append(f"retention-warning:{exc.__class__.__name__}")
|
|
2292
|
+
|
|
2278
2293
|
return actions
|
|
2279
2294
|
|
|
2280
2295
|
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Deep Sleep runtime retention.
|
|
2
|
+
|
|
3
|
+
Keeps Deep Sleep operational artifacts bounded without touching the live memory
|
|
4
|
+
databases or the local-context index. The policy is intentionally conservative:
|
|
5
|
+
old context dumps are only deleted after their run produced a synthesis or agent
|
|
6
|
+
start packet, so failed or incomplete nights remain debuggable.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Iterable
|
|
18
|
+
|
|
19
|
+
DEFAULT_KEEP_DB_BACKUPS = int(os.environ.get("NEXO_DEEP_SLEEP_KEEP_DB_BACKUPS", "3") or "3")
|
|
20
|
+
DEFAULT_KEEP_CONTEXTS = int(os.environ.get("NEXO_DEEP_SLEEP_KEEP_CONTEXTS", "7") or "7")
|
|
21
|
+
DEFAULT_MAX_LOG_BYTES = int(os.environ.get("NEXO_DEEP_SLEEP_MAX_LOG_BYTES", str(1024 * 1024)) or str(1024 * 1024))
|
|
22
|
+
DEFAULT_RETAINED_LOG_BYTES = int(
|
|
23
|
+
os.environ.get("NEXO_DEEP_SLEEP_RETAINED_LOG_BYTES", str(768 * 1024)) or str(768 * 1024)
|
|
24
|
+
)
|
|
25
|
+
DEBUG_TTL_SECONDS = int(os.environ.get("NEXO_DEEP_SLEEP_DEBUG_TTL_SECONDS", str(7 * 86400)) or str(7 * 86400))
|
|
26
|
+
|
|
27
|
+
_DATE_PREFIX_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})(?:[-T]?(\d{6}))?")
|
|
28
|
+
_DATE_CONTEXT_RE = re.compile(r"^(\d{4}-\d{2}-\d{2}(?:[-T]?\d{6})?)-context\.txt$")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _default_home() -> Path:
|
|
32
|
+
return Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))).expanduser()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _dedupe_existing(paths: Iterable[Path]) -> list[Path]:
|
|
36
|
+
seen: set[Path] = set()
|
|
37
|
+
result: list[Path] = []
|
|
38
|
+
for path in paths:
|
|
39
|
+
try:
|
|
40
|
+
key = path.resolve()
|
|
41
|
+
except OSError:
|
|
42
|
+
key = path
|
|
43
|
+
if key in seen or not path.exists():
|
|
44
|
+
continue
|
|
45
|
+
seen.add(key)
|
|
46
|
+
result.append(path)
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _deep_sleep_dirs(nexo_home: Path) -> list[Path]:
|
|
51
|
+
return _dedupe_existing(
|
|
52
|
+
[
|
|
53
|
+
nexo_home / "runtime" / "operations" / "deep-sleep",
|
|
54
|
+
nexo_home / "operations" / "deep-sleep",
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _log_dirs(nexo_home: Path) -> list[Path]:
|
|
60
|
+
return _dedupe_existing(
|
|
61
|
+
[
|
|
62
|
+
nexo_home / "runtime" / "logs",
|
|
63
|
+
nexo_home / "logs",
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _artifact_sort_key(path: Path) -> tuple[float, str]:
|
|
69
|
+
match = _DATE_PREFIX_RE.match(path.name)
|
|
70
|
+
if match:
|
|
71
|
+
date_part, time_part = match.groups()
|
|
72
|
+
compact = date_part.replace("-", "") + (time_part or "000000")
|
|
73
|
+
try:
|
|
74
|
+
return (float(compact), path.name)
|
|
75
|
+
except ValueError:
|
|
76
|
+
pass
|
|
77
|
+
try:
|
|
78
|
+
return (path.stat().st_mtime, path.name)
|
|
79
|
+
except OSError:
|
|
80
|
+
return (0.0, path.name)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _analyzed_marker_exists(deep_sleep_dir: Path, run_id: str) -> bool:
|
|
84
|
+
markers = [
|
|
85
|
+
deep_sleep_dir / f"{run_id}-agent-start-packet.json",
|
|
86
|
+
deep_sleep_dir / f"{run_id}-synthesis.json",
|
|
87
|
+
deep_sleep_dir / f"{run_id}-applied.json",
|
|
88
|
+
deep_sleep_dir / run_id / "synthesis.json",
|
|
89
|
+
]
|
|
90
|
+
return any(path.exists() and path.stat().st_size > 0 for path in markers)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _path_size(path: Path) -> int:
|
|
94
|
+
try:
|
|
95
|
+
if path.is_dir():
|
|
96
|
+
total = 0
|
|
97
|
+
for child in path.rglob("*"):
|
|
98
|
+
try:
|
|
99
|
+
if child.is_file() or child.is_symlink():
|
|
100
|
+
total += child.stat().st_size
|
|
101
|
+
except OSError:
|
|
102
|
+
continue
|
|
103
|
+
return total
|
|
104
|
+
return path.stat().st_size
|
|
105
|
+
except OSError:
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _delete_path(path: Path, *, apply: bool) -> tuple[bool, int]:
|
|
110
|
+
size = _path_size(path)
|
|
111
|
+
if not apply:
|
|
112
|
+
return True, size
|
|
113
|
+
try:
|
|
114
|
+
if path.is_dir():
|
|
115
|
+
shutil.rmtree(path)
|
|
116
|
+
else:
|
|
117
|
+
path.unlink()
|
|
118
|
+
return True, size
|
|
119
|
+
except FileNotFoundError:
|
|
120
|
+
return False, 0
|
|
121
|
+
except OSError:
|
|
122
|
+
return False, 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _sidecars(path: Path) -> list[Path]:
|
|
126
|
+
candidates = [Path(str(path) + "-wal"), Path(str(path) + "-shm")]
|
|
127
|
+
return [candidate for candidate in candidates if candidate.exists()]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _record_delete(report: dict, path: Path, *, reason: str, apply: bool) -> None:
|
|
131
|
+
ok, size = _delete_path(path, apply=apply)
|
|
132
|
+
if not ok:
|
|
133
|
+
report["warnings"].append(f"delete-failed:{path}")
|
|
134
|
+
return
|
|
135
|
+
report["deleted_count"] += 1
|
|
136
|
+
report["deleted_bytes"] += size
|
|
137
|
+
report["deleted"].append({"path": str(path), "bytes": size, "reason": reason})
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _prune_db_backups(deep_sleep_dir: Path, report: dict, *, keep: int, apply: bool) -> None:
|
|
141
|
+
for family in ("*-backup-nexo.db", "*-backup-cognitive.db"):
|
|
142
|
+
backups = sorted(deep_sleep_dir.glob(family), key=_artifact_sort_key, reverse=True)
|
|
143
|
+
kept = backups[:keep]
|
|
144
|
+
report["kept"].append({"kind": family, "count": len(kept), "root": str(deep_sleep_dir)})
|
|
145
|
+
for backup in backups[keep:]:
|
|
146
|
+
_record_delete(report, backup, reason=f"old-db-backup:{family}", apply=apply)
|
|
147
|
+
for sidecar in _sidecars(backup):
|
|
148
|
+
_record_delete(report, sidecar, reason=f"old-db-backup-sidecar:{family}", apply=apply)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _prune_contexts(deep_sleep_dir: Path, report: dict, *, keep: int, apply: bool) -> None:
|
|
152
|
+
contexts: list[tuple[str, Path]] = []
|
|
153
|
+
for path in deep_sleep_dir.glob("*-context.txt"):
|
|
154
|
+
match = _DATE_CONTEXT_RE.match(path.name)
|
|
155
|
+
if not match:
|
|
156
|
+
continue
|
|
157
|
+
contexts.append((match.group(1), path))
|
|
158
|
+
|
|
159
|
+
contexts.sort(key=lambda item: _artifact_sort_key(item[1]), reverse=True)
|
|
160
|
+
keep_run_ids = {run_id for run_id, _ in contexts[:keep]}
|
|
161
|
+
report["kept"].append({"kind": "context.txt", "count": min(len(contexts), keep), "root": str(deep_sleep_dir)})
|
|
162
|
+
|
|
163
|
+
for run_id, context_file in contexts[keep:]:
|
|
164
|
+
if run_id in keep_run_ids:
|
|
165
|
+
continue
|
|
166
|
+
if not _analyzed_marker_exists(deep_sleep_dir, run_id):
|
|
167
|
+
report["kept"].append({"kind": "unanalyzed-context", "path": str(context_file)})
|
|
168
|
+
continue
|
|
169
|
+
_record_delete(report, context_file, reason="old-analyzed-context", apply=apply)
|
|
170
|
+
date_dir = deep_sleep_dir / run_id
|
|
171
|
+
if date_dir.is_dir():
|
|
172
|
+
_record_delete(report, date_dir, reason="old-analyzed-context-dir", apply=apply)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _prune_debug_scratch(deep_sleep_dir: Path, report: dict, *, now: float, apply: bool) -> None:
|
|
176
|
+
for pattern in ("debug-extract-*.txt", "debug-synthesize-*.txt"):
|
|
177
|
+
for path in deep_sleep_dir.glob(pattern):
|
|
178
|
+
try:
|
|
179
|
+
if now - path.stat().st_mtime <= DEBUG_TTL_SECONDS:
|
|
180
|
+
continue
|
|
181
|
+
except OSError:
|
|
182
|
+
continue
|
|
183
|
+
_record_delete(report, path, reason="old-debug-scratch", apply=apply)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _rotate_log(path: Path, report: dict, *, max_bytes: int, retained_bytes: int, apply: bool) -> None:
|
|
187
|
+
try:
|
|
188
|
+
original_size = path.stat().st_size
|
|
189
|
+
except OSError:
|
|
190
|
+
return
|
|
191
|
+
if original_size <= max_bytes:
|
|
192
|
+
return
|
|
193
|
+
retained_bytes = max(1, min(retained_bytes, max_bytes))
|
|
194
|
+
if not apply:
|
|
195
|
+
report["logs_rotated"] += 1
|
|
196
|
+
report["log_bytes_trimmed"] += max(0, original_size - retained_bytes)
|
|
197
|
+
report["rotated_logs"].append({"path": str(path), "original_bytes": original_size, "dry_run": True})
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
with path.open("rb") as fh:
|
|
202
|
+
if original_size > retained_bytes:
|
|
203
|
+
fh.seek(-retained_bytes, os.SEEK_END)
|
|
204
|
+
tail = fh.read()
|
|
205
|
+
newline = tail.find(b"\n")
|
|
206
|
+
if newline > 0:
|
|
207
|
+
tail = tail[newline + 1 :]
|
|
208
|
+
header = (
|
|
209
|
+
f"[rotated by NEXO Deep Sleep retention; original_bytes={original_size}; "
|
|
210
|
+
f"retained_bytes={len(tail)}]\n"
|
|
211
|
+
).encode("utf-8")
|
|
212
|
+
path.write_bytes(header + tail)
|
|
213
|
+
new_size = path.stat().st_size
|
|
214
|
+
except OSError as exc:
|
|
215
|
+
report["warnings"].append(f"log-rotate-failed:{path}:{exc.__class__.__name__}")
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
report["logs_rotated"] += 1
|
|
219
|
+
report["log_bytes_trimmed"] += max(0, original_size - new_size)
|
|
220
|
+
report["rotated_logs"].append({"path": str(path), "original_bytes": original_size, "new_bytes": new_size})
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _rotate_logs(nexo_home: Path, report: dict, *, max_bytes: int, retained_bytes: int, apply: bool) -> None:
|
|
224
|
+
names = {
|
|
225
|
+
"deep-sleep.log",
|
|
226
|
+
"deep-sleep-stdout.log",
|
|
227
|
+
"deep-sleep-stderr.log",
|
|
228
|
+
"sleep-stdout.log",
|
|
229
|
+
"sleep-stderr.log",
|
|
230
|
+
"prevent-sleep-stdout.log",
|
|
231
|
+
"prevent-sleep-stderr.log",
|
|
232
|
+
}
|
|
233
|
+
for log_dir in _log_dirs(nexo_home):
|
|
234
|
+
for name in names:
|
|
235
|
+
path = log_dir / name
|
|
236
|
+
if path.is_file():
|
|
237
|
+
_rotate_log(path, report, max_bytes=max_bytes, retained_bytes=retained_bytes, apply=apply)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def prune_deep_sleep_runtime(
|
|
241
|
+
*,
|
|
242
|
+
nexo_home: str | Path | None = None,
|
|
243
|
+
apply: bool = False,
|
|
244
|
+
keep_db_backups: int = DEFAULT_KEEP_DB_BACKUPS,
|
|
245
|
+
keep_contexts: int = DEFAULT_KEEP_CONTEXTS,
|
|
246
|
+
max_log_bytes: int = DEFAULT_MAX_LOG_BYTES,
|
|
247
|
+
retained_log_bytes: int = DEFAULT_RETAINED_LOG_BYTES,
|
|
248
|
+
) -> dict:
|
|
249
|
+
"""Apply or plan Deep Sleep retention for a runtime home."""
|
|
250
|
+
home = Path(nexo_home).expanduser() if nexo_home is not None else _default_home()
|
|
251
|
+
keep_db_backups = max(1, int(keep_db_backups))
|
|
252
|
+
keep_contexts = max(1, int(keep_contexts))
|
|
253
|
+
report: dict = {
|
|
254
|
+
"ok": True,
|
|
255
|
+
"apply": bool(apply),
|
|
256
|
+
"nexo_home": str(home),
|
|
257
|
+
"roots": [],
|
|
258
|
+
"deleted_count": 0,
|
|
259
|
+
"deleted_bytes": 0,
|
|
260
|
+
"deleted": [],
|
|
261
|
+
"kept": [],
|
|
262
|
+
"logs_rotated": 0,
|
|
263
|
+
"log_bytes_trimmed": 0,
|
|
264
|
+
"rotated_logs": [],
|
|
265
|
+
"warnings": [],
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
now = time.time()
|
|
269
|
+
for deep_sleep_dir in _deep_sleep_dirs(home):
|
|
270
|
+
report["roots"].append(str(deep_sleep_dir))
|
|
271
|
+
_prune_db_backups(deep_sleep_dir, report, keep=keep_db_backups, apply=apply)
|
|
272
|
+
_prune_contexts(deep_sleep_dir, report, keep=keep_contexts, apply=apply)
|
|
273
|
+
_prune_debug_scratch(deep_sleep_dir, report, now=now, apply=apply)
|
|
274
|
+
_rotate_logs(home, report, max_bytes=max_log_bytes, retained_bytes=retained_log_bytes, apply=apply)
|
|
275
|
+
return report
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _print_human(report: dict) -> None:
|
|
279
|
+
mode = "apply" if report.get("apply") else "dry-run"
|
|
280
|
+
print(f"NEXO Deep Sleep retention ({mode})")
|
|
281
|
+
print(f" roots: {len(report.get('roots') or [])}")
|
|
282
|
+
print(f" deleted: {report.get('deleted_count', 0)}")
|
|
283
|
+
print(f" freed/planned: {report.get('deleted_bytes', 0)} bytes")
|
|
284
|
+
print(f" logs_rotated: {report.get('logs_rotated', 0)}")
|
|
285
|
+
print(f" log_bytes_trimmed: {report.get('log_bytes_trimmed', 0)}")
|
|
286
|
+
if report.get("warnings"):
|
|
287
|
+
print(" warnings:")
|
|
288
|
+
for warning in report["warnings"]:
|
|
289
|
+
print(f" - {warning}")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def main(argv: list[str] | None = None) -> int:
|
|
293
|
+
parser = argparse.ArgumentParser(description="NEXO Deep Sleep retention")
|
|
294
|
+
parser.add_argument("--nexo-home", default=None)
|
|
295
|
+
parser.add_argument("--apply", action="store_true", help="delete/rotate instead of dry-run")
|
|
296
|
+
parser.add_argument("--json", action="store_true", help="print machine-readable JSON")
|
|
297
|
+
parser.add_argument("--quiet", action="store_true", help="suppress human output")
|
|
298
|
+
parser.add_argument("--keep-db-backups", type=int, default=DEFAULT_KEEP_DB_BACKUPS)
|
|
299
|
+
parser.add_argument("--keep-contexts", type=int, default=DEFAULT_KEEP_CONTEXTS)
|
|
300
|
+
parser.add_argument("--max-log-bytes", type=int, default=DEFAULT_MAX_LOG_BYTES)
|
|
301
|
+
parser.add_argument("--retained-log-bytes", type=int, default=DEFAULT_RETAINED_LOG_BYTES)
|
|
302
|
+
args = parser.parse_args(argv)
|
|
303
|
+
|
|
304
|
+
report = prune_deep_sleep_runtime(
|
|
305
|
+
nexo_home=args.nexo_home,
|
|
306
|
+
apply=args.apply,
|
|
307
|
+
keep_db_backups=args.keep_db_backups,
|
|
308
|
+
keep_contexts=args.keep_contexts,
|
|
309
|
+
max_log_bytes=args.max_log_bytes,
|
|
310
|
+
retained_log_bytes=args.retained_log_bytes,
|
|
311
|
+
)
|
|
312
|
+
if args.json:
|
|
313
|
+
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
314
|
+
elif not args.quiet:
|
|
315
|
+
_print_human(report)
|
|
316
|
+
return 0 if report.get("ok") else 1
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
SRC_DIR = Path(__file__).resolve().parents[2]
|
|
8
|
+
if str(SRC_DIR) not in sys.path:
|
|
9
|
+
sys.path.insert(0, str(SRC_DIR))
|
|
10
|
+
|
|
11
|
+
from deep_sleep_retention import main
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
raise SystemExit(main())
|
|
@@ -23,6 +23,14 @@ mkdir -p "$LOG_DIR" "$DEEP_SLEEP_DIR"
|
|
|
23
23
|
|
|
24
24
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_DIR/deep-sleep.log"; }
|
|
25
25
|
|
|
26
|
+
run_retention() {
|
|
27
|
+
if ! python3 "$SCRIPT_DIR/deep-sleep/retention.py" --apply --quiet >> "$LOG_DIR/deep-sleep.log" 2>&1; then
|
|
28
|
+
log "Retention warning: Deep Sleep cleanup failed"
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
run_retention
|
|
33
|
+
|
|
26
34
|
# Read watermark (last processed timestamp)
|
|
27
35
|
SINCE=""
|
|
28
36
|
if [ -f "$WATERMARK_FILE" ]; then
|
|
@@ -46,6 +54,7 @@ if [ ! -f "$DEEP_SLEEP_DIR/$RUN_ID-context.txt" ]; then
|
|
|
46
54
|
log "No context file generated. Skipping."
|
|
47
55
|
echo "$UNTIL" > "$WATERMARK_FILE"
|
|
48
56
|
log "Watermark updated to $UNTIL (no sessions to process)"
|
|
57
|
+
run_retention
|
|
49
58
|
exit 0
|
|
50
59
|
fi
|
|
51
60
|
|
|
@@ -58,6 +67,7 @@ if [ "$SESSIONS" -eq 0 ]; then
|
|
|
58
67
|
log "No sessions found. Skipping."
|
|
59
68
|
echo "$UNTIL" > "$WATERMARK_FILE"
|
|
60
69
|
log "Watermark updated to $UNTIL (no sessions)"
|
|
70
|
+
run_retention
|
|
61
71
|
exit 0
|
|
62
72
|
fi
|
|
63
73
|
|
|
@@ -96,3 +106,4 @@ fi
|
|
|
96
106
|
echo "$UNTIL" > "$WATERMARK_FILE"
|
|
97
107
|
log "Watermark updated to $UNTIL"
|
|
98
108
|
log "=== Deep Sleep v2 complete (run_id=$RUN_ID) ==="
|
|
109
|
+
run_retention
|
|
@@ -41,6 +41,7 @@ if str(NEXO_CODE) not in sys.path:
|
|
|
41
41
|
from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
|
|
42
42
|
from constants import AUTOMATION_SUBPROCESS_TIMEOUT
|
|
43
43
|
from core_prompts import render_core_prompt
|
|
44
|
+
from deep_sleep_retention import prune_deep_sleep_runtime
|
|
44
45
|
import paths
|
|
45
46
|
try:
|
|
46
47
|
from client_preferences import resolve_user_model as _resolve_user_model
|
|
@@ -261,6 +262,9 @@ def stage_a_cleanup() -> dict:
|
|
|
261
262
|
"a5_heartbeat_trimmed": False,
|
|
262
263
|
"a6_reflection_trimmed": False,
|
|
263
264
|
"a7_daemon_logs_deleted": 0,
|
|
265
|
+
"a9_deep_sleep_deleted": 0,
|
|
266
|
+
"a9_deep_sleep_freed_bytes": 0,
|
|
267
|
+
"a9_deep_sleep_logs_rotated": 0,
|
|
264
268
|
}
|
|
265
269
|
|
|
266
270
|
# A1: Delete daily_summaries/*.md >90 days
|
|
@@ -366,6 +370,16 @@ def stage_a_cleanup() -> dict:
|
|
|
366
370
|
except Exception:
|
|
367
371
|
pass
|
|
368
372
|
|
|
373
|
+
# A9: Bound Deep Sleep operational artifacts and logs. This is safe to run
|
|
374
|
+
# daily: incomplete/unanalysed contexts are preserved for retry/debugging.
|
|
375
|
+
try:
|
|
376
|
+
deep_sleep_retention = prune_deep_sleep_runtime(nexo_home=NEXO_HOME, apply=True)
|
|
377
|
+
stats["a9_deep_sleep_deleted"] = int(deep_sleep_retention.get("deleted_count") or 0)
|
|
378
|
+
stats["a9_deep_sleep_freed_bytes"] = int(deep_sleep_retention.get("deleted_bytes") or 0)
|
|
379
|
+
stats["a9_deep_sleep_logs_rotated"] = int(deep_sleep_retention.get("logs_rotated") or 0)
|
|
380
|
+
except Exception as exc:
|
|
381
|
+
stats["a9_deep_sleep_warning"] = exc.__class__.__name__
|
|
382
|
+
|
|
369
383
|
return stats
|
|
370
384
|
|
|
371
385
|
|