nexo-brain 3.0.1 → 3.0.2

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.
@@ -21,10 +21,14 @@ def handle_backup_now() -> str:
21
21
  # Use SQLite backup API for consistency
22
22
  import sqlite3
23
23
  src_conn = sqlite3.connect(DB_PATH)
24
- dst_conn = sqlite3.connect(dest)
25
- src_conn.backup(dst_conn)
26
- dst_conn.close()
27
- src_conn.close()
24
+ try:
25
+ dst_conn = sqlite3.connect(dest)
26
+ try:
27
+ src_conn.backup(dst_conn)
28
+ finally:
29
+ dst_conn.close()
30
+ finally:
31
+ src_conn.close()
28
32
 
29
33
  size_kb = os.path.getsize(dest) / 1024
30
34
  _cleanup_old()
@@ -63,17 +67,25 @@ def handle_backup_restore(filename: str) -> str:
63
67
  safety = os.path.join(BACKUP_DIR, f"nexo-pre-restore-{time.strftime('%Y%m%d%H%M%S')}.db")
64
68
  import sqlite3
65
69
  src_conn = sqlite3.connect(DB_PATH)
66
- dst_conn = sqlite3.connect(safety)
67
- src_conn.backup(dst_conn)
68
- dst_conn.close()
69
- src_conn.close()
70
+ try:
71
+ dst_conn = sqlite3.connect(safety)
72
+ try:
73
+ src_conn.backup(dst_conn)
74
+ finally:
75
+ dst_conn.close()
76
+ finally:
77
+ src_conn.close()
70
78
 
71
79
  # Restore
72
80
  restore_conn = sqlite3.connect(src)
73
- target_conn = sqlite3.connect(DB_PATH)
74
- restore_conn.backup(target_conn)
75
- target_conn.close()
76
- restore_conn.close()
81
+ try:
82
+ target_conn = sqlite3.connect(DB_PATH)
83
+ try:
84
+ restore_conn.backup(target_conn)
85
+ finally:
86
+ target_conn.close()
87
+ finally:
88
+ restore_conn.close()
77
89
 
78
90
  # Invalidate shared connection so db.py reconnects to restored data
79
91
  import db
@@ -3,6 +3,7 @@
3
3
  import json
4
4
  import os
5
5
  import platform
6
+ import re
6
7
  import subprocess
7
8
  from pathlib import Path
8
9
 
@@ -69,9 +70,20 @@ def _summary_has_warning(summary: str = "") -> bool:
69
70
  lowered = str(summary or "").strip().lower()
70
71
  if not lowered:
71
72
  return False
73
+ if re.search(r"\b[1-9]\d*\s+(errors?|warnings?)\b", lowered):
74
+ return True
72
75
  if "no warning" in lowered or "without warnings" in lowered:
73
76
  return False
74
- warning_tokens = ("warning", "warnings", "warn:", "degraded", "partial failure", "issues detected")
77
+ warning_tokens = (
78
+ "warning",
79
+ "warnings",
80
+ "warn:",
81
+ "degraded",
82
+ "partial failure",
83
+ "issues detected",
84
+ "completed with findings",
85
+ "findings detected",
86
+ )
75
87
  return any(token in lowered for token in warning_tokens)
76
88
 
77
89
 
@@ -149,14 +149,21 @@ def _backup_databases() -> tuple[str, str | None]:
149
149
 
150
150
  for db_file in db_files:
151
151
  dest = backup_dir / db_file.name
152
+ src_conn = None
153
+ dst_conn = None
152
154
  try:
153
155
  src_conn = sqlite3.connect(str(db_file))
154
156
  dst_conn = sqlite3.connect(str(dest))
155
157
  src_conn.backup(dst_conn)
156
- dst_conn.close()
157
- src_conn.close()
158
158
  except Exception as e:
159
159
  return str(backup_dir), f"Failed to backup {db_file.name}: {e}"
160
+ finally:
161
+ for conn in (dst_conn, src_conn):
162
+ if conn is not None:
163
+ try:
164
+ conn.close()
165
+ except Exception:
166
+ pass
160
167
 
161
168
  return str(backup_dir), None
162
169
 
@@ -170,14 +177,21 @@ def _restore_databases(backup_dir: str):
170
177
  # Try to find original location
171
178
  for candidate in [DATA_DIR / db_backup.name, NEXO_HOME / db_backup.name, SRC_DIR / db_backup.name]:
172
179
  if candidate.is_file():
180
+ src_conn = None
181
+ dst_conn = None
173
182
  try:
174
183
  src_conn = sqlite3.connect(str(db_backup))
175
184
  dst_conn = sqlite3.connect(str(candidate))
176
185
  src_conn.backup(dst_conn)
177
- dst_conn.close()
178
- src_conn.close()
179
186
  except Exception:
180
187
  pass
188
+ finally:
189
+ for conn in (dst_conn, src_conn):
190
+ if conn is not None:
191
+ try:
192
+ conn.close()
193
+ except Exception:
194
+ pass
181
195
  break
182
196
 
183
197
 
@@ -14,7 +14,7 @@ import re
14
14
  import shutil
15
15
  import socket
16
16
  import subprocess
17
- from datetime import datetime, timedelta, timezone
17
+ from datetime import datetime, timezone
18
18
  from pathlib import Path
19
19
 
20
20
  from runtime_power import load_schedule_config, save_schedule_config
@@ -33,8 +33,6 @@ STATUS_PENDING_AUTH = "pending_auth"
33
33
  STATUS_PAUSED_OPEN_PR = "paused_open_pr"
34
34
  STATUS_COOLDOWN = "cooldown"
35
35
  STATUS_OFF = "off"
36
- COOLDOWN_HOURS_MERGED = 168
37
- COOLDOWN_HOURS_CLOSED = 72
38
36
  VALID_MODES = {MODE_UNSET, MODE_OFF, MODE_DRAFT_PRS, MODE_PENDING_AUTH}
39
37
  VALID_STATUSES = {
40
38
  STATUS_UNSET,
@@ -224,10 +222,6 @@ def _parse_iso(ts: str | None) -> datetime | None:
224
222
  return None
225
223
 
226
224
 
227
- def _future_iso(hours: int) -> str:
228
- return (_utcnow() + timedelta(hours=hours)).isoformat()
229
-
230
-
231
225
  def format_public_contribution_label(config: dict | None = None) -> str:
232
226
  cfg = normalize_public_contribution_config(config)
233
227
  if cfg["mode"] == MODE_DRAFT_PRS:
@@ -369,12 +363,13 @@ def refresh_public_contribution_state(config: dict | None = None) -> dict:
369
363
  config["status"] = STATUS_PAUSED_OPEN_PR
370
364
  save_public_contribution_config(config)
371
365
  return config
366
+ resolution = "merged" if payload.get("mergedAt") else "closed"
372
367
  config["active_pr_url"] = ""
373
368
  config["active_pr_number"] = None
374
369
  config["active_branch"] = ""
375
- cooldown_hours = COOLDOWN_HOURS_MERGED if payload.get("mergedAt") else COOLDOWN_HOURS_CLOSED
376
- config["cooldown_until"] = _future_iso(cooldown_hours)
377
- config["status"] = STATUS_COOLDOWN
370
+ config["cooldown_until"] = ""
371
+ config["status"] = STATUS_ACTIVE
372
+ config["last_result"] = f"resolved_pr:{resolution}:{payload.get('url') or ''}".rstrip(":")
378
373
  save_public_contribution_config(config)
379
374
  return config
380
375
  return _set_pending_auth(
@@ -384,7 +379,11 @@ def refresh_public_contribution_state(config: dict | None = None) -> dict:
384
379
 
385
380
  cooldown_until = _parse_iso(config.get("cooldown_until"))
386
381
  if cooldown_until and cooldown_until > _utcnow():
387
- config["status"] = STATUS_COOLDOWN
382
+ # Legacy installs used a post-merge/close cooldown that blocked the next
383
+ # public contribution cycle even after maintainers resolved the Draft PR.
384
+ # Public contribution should pause only while the PR is still open.
385
+ config["cooldown_until"] = ""
386
+ config["status"] = STATUS_ACTIVE
388
387
  save_public_contribution_config(config)
389
388
  return config
390
389
 
@@ -430,9 +429,6 @@ def can_run_public_contribution(config: dict | None = None) -> tuple[bool, str,
430
429
  return False, "public contribution is disabled", config
431
430
  if config["status"] == STATUS_PAUSED_OPEN_PR:
432
431
  return False, "an active Draft PR is already open for this machine", config
433
- cooldown_until = _parse_iso(config.get("cooldown_until"))
434
- if cooldown_until and cooldown_until > _utcnow():
435
- return False, f"cooldown until {cooldown_until.isoformat()}", config
436
432
  return True, "", config
437
433
 
438
434
 
@@ -197,14 +197,14 @@ def main():
197
197
  log("Catch-Up already running; skipping overlapping invocation.")
198
198
  return
199
199
 
200
- _heal_personal_schedules()
201
- state = load_state()
202
- tasks = catchup_candidates()
203
-
204
200
  ran = 0
205
201
  skipped = 0
206
202
  skipped_out_of_window = 0
207
203
  try:
204
+ _heal_personal_schedules()
205
+ state = load_state()
206
+ tasks = catchup_candidates()
207
+
208
208
  for candidate in tasks:
209
209
  name = candidate["cron_id"]
210
210
  if not candidate.get("missed"):
@@ -221,19 +221,19 @@ def main():
221
221
  log(f" {name} — missed scheduled run due at {due_at}, catching up...")
222
222
  if run_task(candidate, state):
223
223
  ran += 1
224
- finally:
225
- lock_handle.close()
226
224
 
227
- if ran == 0 and skipped_out_of_window == 0:
228
- log("All tasks up to date, nothing to catch up.")
229
- elif ran >= 3:
230
- # Many tasks caught up — ask CLI to assess system state
231
- _cli_post_catchup_assessment(ran, skipped, state)
232
- else:
233
- suffix = f", {skipped_out_of_window} outside recovery window" if skipped_out_of_window else ""
234
- log(f"Caught up {ran} tasks, {skipped} already current{suffix}.")
225
+ if ran == 0 and skipped_out_of_window == 0:
226
+ log("All tasks up to date, nothing to catch up.")
227
+ elif ran >= 3:
228
+ # Many tasks caught up — ask CLI to assess system state
229
+ _cli_post_catchup_assessment(ran, skipped, state)
230
+ else:
231
+ suffix = f", {skipped_out_of_window} outside recovery window" if skipped_out_of_window else ""
232
+ log(f"Caught up {ran} tasks, {skipped} already current{suffix}.")
235
233
 
236
- log("=== Catch-Up complete ===")
234
+ log("=== Catch-Up complete ===")
235
+ finally:
236
+ lock_handle.close()
237
237
 
238
238
 
239
239
  def _cli_post_catchup_assessment(ran: int, skipped: int, state: dict):
@@ -1407,8 +1407,19 @@ def main():
1407
1407
  except Exception:
1408
1408
  pass
1409
1409
 
1410
+ if errors or warns:
1411
+ log(
1412
+ f"Self-audit completed with findings: {errors} errors, {warns} warnings, {infos} info. "
1413
+ f"Summary written to {summary_file}."
1414
+ )
1415
+ else:
1416
+ log(
1417
+ f"Self-audit completed cleanly: {errors} errors, {warns} warnings, {infos} info. "
1418
+ f"Summary written to {summary_file}."
1419
+ )
1420
+
1410
1421
  log("=" * 60)
1411
- return 1 if errors > 0 else 0
1422
+ return 0
1412
1423
 
1413
1424
 
1414
1425
  if __name__ == "__main__":
@@ -364,8 +364,6 @@ def _sanitize_public_diff(worktree_dir: Path, changed_files: list[str]) -> tuple
364
364
  private_markers = [
365
365
  str(Path.home()),
366
366
  str(NEXO_HOME),
367
- "/Users/",
368
- "/home/",
369
367
  "CLAUDE.md",
370
368
  "AGENTS.md",
371
369
  ".nexo/",
@@ -374,6 +372,14 @@ def _sanitize_public_diff(worktree_dir: Path, changed_files: list[str]) -> tuple
374
372
  for marker in private_markers:
375
373
  if marker and marker in diff_text:
376
374
  return False, f"Sanitization blocked private marker in diff: {marker}"
375
+ private_path_patterns = [
376
+ re.compile(r"/Users/[^/\s\"']+/"),
377
+ re.compile(r"/home/[^/\s\"']+/"),
378
+ ]
379
+ for pattern in private_path_patterns:
380
+ match = pattern.search(diff_text)
381
+ if match:
382
+ return False, f"Sanitization blocked private path in diff: {match.group(0)}"
377
383
  return True, ""
378
384
 
379
385
 
@@ -865,7 +871,7 @@ def run_public_contribution_cycle(*, objective: dict, cycle_num: int) -> None:
865
871
 
866
872
  pr_url, pr_number = _create_draft_pr(worktree_dir, config, branch_name, summary)
867
873
  artifact_dir = _write_public_artifacts(worktree_dir, branch_name, summary)
868
- mark_active_pr(pr_url=pr_url, pr_number=pr_number, branch=branch_name, config=config)
874
+ config = mark_active_pr(pr_url=pr_url, pr_number=pr_number, branch=branch_name, config=config)
869
875
 
870
876
  conn.execute(
871
877
  "INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, reasoning, status, files_changed, test_result) "
@@ -70,11 +70,15 @@ def _load_last_cron_run(cron_id: str) -> datetime | None:
70
70
  if not db_path.is_file():
71
71
  return None
72
72
  conn = sqlite3.connect(str(db_path))
73
- row = conn.execute(
74
- "SELECT started_at FROM cron_runs WHERE cron_id = ? ORDER BY started_at DESC LIMIT 1",
75
- (cron_id,),
76
- ).fetchone()
77
- conn.close()
73
+ try:
74
+ row = conn.execute(
75
+ "SELECT started_at FROM cron_runs WHERE cron_id = ? ORDER BY started_at DESC LIMIT 1",
76
+ (cron_id,),
77
+ ).fetchone()
78
+ except Exception:
79
+ return None
80
+ finally:
81
+ conn.close()
78
82
  if not row or not row[0]:
79
83
  return None
80
84
  return _parse_dt(str(row[0]))
@@ -263,25 +267,26 @@ def _list_watchers(*, status: str) -> list[dict]:
263
267
  if not db_path.is_file():
264
268
  return []
265
269
  conn = sqlite3.connect(str(db_path))
266
- conn.row_factory = sqlite3.Row
267
- clauses = []
268
- params = []
269
- if status:
270
- clauses.append("status = ?")
271
- params.append(status)
272
- where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
273
270
  try:
274
- rows = conn.execute(
275
- f"""SELECT watcher_id, watcher_type, title, target, severity, status, config, last_health, last_result, last_checked_at
276
- FROM state_watchers
277
- {where}
278
- ORDER BY updated_at DESC, watcher_id DESC""",
279
- tuple(params),
280
- ).fetchall()
281
- except sqlite3.OperationalError:
271
+ conn.row_factory = sqlite3.Row
272
+ clauses = []
273
+ params = []
274
+ if status:
275
+ clauses.append("status = ?")
276
+ params.append(status)
277
+ where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
278
+ try:
279
+ rows = conn.execute(
280
+ f"""SELECT watcher_id, watcher_type, title, target, severity, status, config, last_health, last_result, last_checked_at
281
+ FROM state_watchers
282
+ {where}
283
+ ORDER BY updated_at DESC, watcher_id DESC""",
284
+ tuple(params),
285
+ ).fetchall()
286
+ except sqlite3.OperationalError:
287
+ return []
288
+ finally:
282
289
  conn.close()
283
- return []
284
- conn.close()
285
290
  watchers = []
286
291
  for row in rows:
287
292
  watcher = dict(row)
@@ -298,19 +303,21 @@ def _persist_result(result: dict) -> None:
298
303
  if not db_path.is_file():
299
304
  return
300
305
  conn = sqlite3.connect(str(db_path))
301
- conn.execute(
302
- """UPDATE state_watchers
303
- SET last_health = ?, last_result = ?, last_checked_at = ?, updated_at = datetime('now')
304
- WHERE watcher_id = ?""",
305
- (
306
- result["health"],
307
- json.dumps(result, ensure_ascii=False),
308
- result["checked_at"],
309
- result["watcher_id"],
310
- ),
311
- )
312
- conn.commit()
313
- conn.close()
306
+ try:
307
+ conn.execute(
308
+ """UPDATE state_watchers
309
+ SET last_health = ?, last_result = ?, last_checked_at = ?, updated_at = datetime('now')
310
+ WHERE watcher_id = ?""",
311
+ (
312
+ result["health"],
313
+ json.dumps(result, ensure_ascii=False),
314
+ result["checked_at"],
315
+ result["watcher_id"],
316
+ ),
317
+ )
318
+ conn.commit()
319
+ finally:
320
+ conn.close()
314
321
 
315
322
 
316
323
  def run_state_watchers(*, persist: bool = True, status: str = "active") -> dict: