nexo-brain 0.2.1 → 0.3.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.
@@ -9,7 +9,7 @@ and writes permanent rules to memory files so they survive forever.
9
9
  Three layers:
10
10
  1. Session → self_critique field in session_diary (captured at session end)
11
11
  2. Daily → this script consolidates all critiques from today
12
- 3. Permanent → writes to feedback_*.md files
12
+ 3. Permanent → writes to feedback_*.md files + MEMORY.md index
13
13
 
14
14
  Only creates permanent memory for patterns that:
15
15
  - Appear 2+ times in the same day, OR
@@ -26,23 +26,25 @@ from collections import Counter
26
26
  from datetime import datetime, date, timedelta
27
27
  from pathlib import Path
28
28
 
29
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
30
- sys.path.insert(0, str(NEXO_HOME / "src"))
29
+ # Add nexo-mcp to path for cognitive engine
30
+ sys.path.insert(0, str(Path.home() / "claude" / "nexo-mcp"))
31
31
 
32
32
  HOME = Path.home()
33
- NEXO_DB = NEXO_HOME / "nexo.db"
34
- SESSION_BUFFER = NEXO_HOME / "brain" / "session_buffer.jsonl"
35
- MEMORY_DIR = NEXO_HOME / "memory"
36
- CONSOLIDATION_LOG = NEXO_HOME / "logs" / "postmortem-consolidation.log"
37
- HISTORY_FILE = NEXO_HOME / "coordination" / "postmortem-history.json"
33
+ NEXO_DB = HOME / "claude" / "nexo-mcp" / "nexo.db"
34
+ SESSION_BUFFER = HOME / "claude" / "brain" / "session_buffer.jsonl"
35
+ MEMORY_DIR = HOME / ".claude" / "projects" / f"-Users-{os.environ.get('USER', 'user')}" / "memory"
36
+ MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
37
+ CONSOLIDATION_LOG = HOME / "claude" / "logs" / "postmortem-consolidation.log"
38
+ HISTORY_FILE = HOME / "claude" / "coordination" / "postmortem-history.json"
38
39
 
39
40
  TODAY = date.today()
40
41
  TODAY_STR = TODAY.isoformat()
41
42
 
42
43
  CORRECTION_KEYWORDS = [
43
- "corrected", "wrong again", "already told", "repeating", "should not",
44
- "why not", "again", "tired", "always waiting", "not proactive", "reactive",
45
- "not doing", "error", "wrong", "failure", "frustrat"
44
+ "corrig", "frustrad", "no lo entiend", "exig", "repet",
45
+ "no debería", "por qué no", "otra vez", "ya te dije",
46
+ "cansando", "siempre espera", "no te adelant", "reactivo",
47
+ "no haces", "error", "mal", "fallo", "irritad"
46
48
  ]
47
49
 
48
50
 
@@ -99,13 +101,13 @@ def extract_actionable_rules(critiques: list[str]) -> list[str]:
99
101
  """Extract concrete, actionable rules from self-critique text."""
100
102
  rules = []
101
103
  for critique in critiques:
102
- if not critique or critique.strip().lower().startswith("no self-critique"):
104
+ if not critique or critique.strip().lower().startswith("sin autocrítica"):
103
105
  continue
104
106
  # Each non-empty critique is a potential rule
105
107
  # Clean up and normalize
106
108
  for line in critique.split("\n"):
107
109
  line = line.strip().lstrip("- ").strip()
108
- if len(line) > 20:
110
+ if len(line) > 20 and not line.lower().startswith("sin "):
109
111
  rules.append(line)
110
112
  return rules
111
113
 
@@ -155,23 +157,22 @@ def write_permanent_rule(rule_title: str, rule_content: str, source_critiques: l
155
157
  log(f" File already exists: {filename}, skipping")
156
158
  return None
157
159
 
158
- MEMORY_DIR.mkdir(parents=True, exist_ok=True)
159
160
  content = f"""---
160
161
  name: {rule_title}
161
- description: Behavioral rule extracted from post-mortem self-critique recurring pattern detected
162
+ description: Regla de comportamiento extraída de autocrítica post-mortem — patrón recurrente detectado
162
163
  type: feedback
163
164
  ---
164
165
 
165
166
  {rule_content}
166
167
 
167
- **Why:** Pattern detected across multiple sessions where NEXO failed in this aspect. The user should not have to correct the same issue twice.
168
+ **Why:** Patrón detectado en múltiples sesiones donde NEXO falló en este aspecto. The user should not tener que corregir lo mismo dos veces.
168
169
 
169
- **How to apply:** Verify this rule at the start of each session and before presenting work as complete.
170
+ **How to apply:** Verificar esta regla al inicio de cada sesión y antes de presentar trabajo como completado.
170
171
 
171
- **Evidence (original self-critiques):**
172
+ **Evidencia (autocríticas originales):**
172
173
  """
173
174
  for i, critique in enumerate(source_critiques[:3], 1):
174
- content += f"- Session {i}: {critique[:200]}\n"
175
+ content += f"- Sesión {i}: {critique[:200]}\n"
175
176
 
176
177
  filepath.write_text(content)
177
178
  log(f" Written permanent rule: {filename}")
@@ -270,12 +271,14 @@ def process_sensory_register():
270
271
  "matches": patterns[:3],
271
272
  })
272
273
 
273
- # Ingest into STM as sensory — detect domain from content keywords
274
+ # Ingest into STM as sensory
275
+ # Customize domain keywords for your own projects
274
276
  domain = ""
275
- content_lower = content.lower()
276
- # Domain detection is generic — users can extend this
277
- if any(w in content_lower for w in ["nexo", "cognitive", "guard"]):
277
+ if any(w in content.lower() for w in ["nexo", "cognitive", "guard"]):
278
278
  domain = "nexo"
279
+ # Add your own project domains here:
280
+ # elif any(w in content.lower() for w in ["myproject", "keyword"]):
281
+ # domain = "myproject"
279
282
 
280
283
  cognitive.ingest_sensory(
281
284
  content=content,
@@ -310,7 +313,7 @@ def archive_sensory_buffer():
310
313
  return
311
314
 
312
315
  cutoff = (datetime.now() - timedelta(hours=48)).isoformat()
313
- archive_dir = NEXO_HOME / "brain" / "session_archive"
316
+ archive_dir = HOME / "claude" / "brain" / "session_archive"
314
317
  archive_dir.mkdir(parents=True, exist_ok=True)
315
318
 
316
319
  keep_lines = []
@@ -358,7 +361,7 @@ def archive_sensory_buffer():
358
361
  def analyze_force_events():
359
362
  """Analyze --force dissonance resolutions from today.
360
363
 
361
- When user uses --force, NEXO obeyed without discussion. The nocturnal
364
+ When the user uses --force, NEXO obeyed without discussion. The nocturnal
362
365
  process must now ask: was the old memory wrong, or was the user taking
363
366
  conscious technical debt?
364
367
 
@@ -412,7 +415,7 @@ def analyze_force_events():
412
415
  log(f" PARADIGM SHIFT CANDIDATE: LTM #{mem_id} force-overridden {total_overrides}x total")
413
416
  log(f" Content: {mem['content'][:120]}")
414
417
  log(f" Action: Decaying strength from {mem['strength']:.2f} to 0.3")
415
- # Auto-decay — if it's been overridden 3+ times, user clearly disagrees
418
+ # Auto-decay — if it's been overridden 3+ times, User clearly disagrees
416
419
  db.execute(
417
420
  "UPDATE ltm_memories SET strength = 0.3, tags = CASE WHEN tags LIKE '%paradigm_candidate%' THEN tags ELSE tags || ',paradigm_candidate' END WHERE id = ?",
418
421
  (mem_id,)
@@ -444,7 +447,7 @@ def main():
444
447
  critique = d.get("self_critique") or ""
445
448
  signals = d.get("user_signals") or ""
446
449
 
447
- if critique and not critique.strip().lower().startswith("no self-critique"):
450
+ if critique and not critique.strip().lower().startswith("sin autocrítica"):
448
451
  today_critiques.append(critique)
449
452
 
450
453
  if has_correction_signals(signals):
@@ -487,8 +490,8 @@ def main():
487
490
  overlap = len(words_i & words_j) / min(len(words_i), len(words_j))
488
491
  if overlap > 0.5 and not rule_already_permanent(rule, history):
489
492
  new_permanent.append({
490
- "title": f"Repeated pattern: {rule[:60]}",
491
- "content": f"Detected 2+ times in the same day:\n- {rule}\n- {other}",
493
+ "title": f"Patrón repetido: {rule[:60]}",
494
+ "content": f"Detectado 2+ veces en el mismo día:\n- {rule}\n- {other}",
492
495
  "sources": [rule, other],
493
496
  })
494
497
 
@@ -512,8 +515,8 @@ def main():
512
515
 
513
516
  if len(matching_days) >= 2 and not rule_already_permanent(today_rule, history): # 2 historical + today = 3
514
517
  new_permanent.append({
515
- "title": f"Recurring pattern ({len(matching_days)+1} days): {today_rule[:50]}",
516
- "content": f"Detected on {len(matching_days)+1} different days:\n- Today: {today_rule}\n- Previous days: {', '.join(sorted(matching_days)[:5])}",
518
+ "title": f"Patrón recurrente ({len(matching_days)+1} días): {today_rule[:50]}",
519
+ "content": f"Detectado en {len(matching_days)+1} días diferentes:\n- Hoy: {today_rule}\n- Días previos: {', '.join(sorted(matching_days)[:5])}",
517
520
  "sources": [today_rule],
518
521
  })
519
522
 
@@ -523,7 +526,7 @@ def main():
523
526
  if critique and not rule_already_permanent(critique, history):
524
527
  new_permanent.append({
525
528
  "title": f"User correction: {critique[:50]}",
526
- "content": f"User explicitly corrected this behavior.\nSignals: {cc['signals'][:200]}\nSelf-critique: {critique[:300]}",
529
+ "content": f"The user corrected explícitamente este comportamiento.\nSeñales: {cc['signals'][:200]}\nAutocrítica: {critique[:300]}",
527
530
  "sources": [critique],
528
531
  })
529
532
 
@@ -537,26 +540,25 @@ def main():
537
540
  else:
538
541
  log("No patterns qualify for permanent promotion today.")
539
542
 
540
- # Write daily summary
541
- summary_file = NEXO_HOME / "coordination" / "postmortem-daily.md"
543
+ # Write daily summary to synthesis
544
+ summary_file = HOME / "claude" / "coordination" / "postmortem-daily.md"
542
545
  summary_lines = [
543
546
  f"# Post-Mortem Daily — {TODAY_STR}",
544
- f"Sessions: {len(diaries)} | Self-critiques: {len(today_critiques)} | User corrections: {len(correction_critiques)}",
547
+ f"Sesiones: {len(diaries)} | Autocríticas: {len(today_critiques)} | User corrections: {len(correction_critiques)}",
545
548
  "",
546
549
  ]
547
550
  if today_critiques:
548
- summary_lines.append("## Today's Self-Critiques")
551
+ summary_lines.append("## Autocríticas del día")
549
552
  for c in today_critiques:
550
553
  summary_lines.append(f"- {c[:200]}")
551
554
  summary_lines.append("")
552
555
  if new_permanent:
553
- summary_lines.append("## Promoted to Permanent Memory")
556
+ summary_lines.append("## Promovido a memoria permanente")
554
557
  for r in new_permanent:
555
558
  summary_lines.append(f"- {r['title']}")
556
559
  else:
557
- summary_lines.append("## Nothing promoted today")
560
+ summary_lines.append("## Nada promovido hoy")
558
561
 
559
- summary_file.parent.mkdir(parents=True, exist_ok=True)
560
562
  summary_file.write_text("\n".join(summary_lines))
561
563
  log(f"Written daily summary: {summary_file}")
562
564
 
@@ -576,7 +578,7 @@ def main():
576
578
 
577
579
  # Register successful run for catch-up
578
580
  try:
579
- state_file = NEXO_HOME / "operations" / ".catchup-state.json"
581
+ state_file = HOME / "claude" / "operations" / ".catchup-state.json"
580
582
  state = json.loads(state_file.read_text()) if state_file.exists() else {}
581
583
  state["postmortem"] = datetime.now().isoformat()
582
584
  state_file.write_text(json.dumps(state, indent=2))
@@ -21,8 +21,8 @@ Stage C — Learning Consolidation (Python pure, always runs):
21
21
  C4: Contradiction detection (NUNCA pairs in same category)
22
22
 
23
23
  Stage B — Intelligent pruning (Claude CLI, conditional):
24
- Only activates if nexo.db preferences table has >5 rows,
25
- or other configurable thresholds are exceeded.
24
+ Only activates if MEMORY.md >170 lines, nexo.db preferences table has >5 rows,
25
+ or claude-mem.db has >500 observations >60 days.
26
26
  Uses Claude CLI (sonnet) to compress and prune.
27
27
 
28
28
  Zero external dependencies beyond stdlib + sqlite3. Claude CLI for Stage B only.
@@ -39,13 +39,12 @@ import sys
39
39
  from datetime import datetime, date, timedelta
40
40
  from pathlib import Path
41
41
 
42
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
43
-
44
42
  # ─── Paths ────────────────────────────────────────────────────────────────────
45
- BRAIN_DIR = NEXO_HOME / "brain"
46
- COORD_DIR = NEXO_HOME / "coordination"
47
- MEMORY_DIR = NEXO_HOME / "memory"
48
- DAEMON_LOGS_DIR = NEXO_HOME / "daemon" / "logs"
43
+ CLAUDE_DIR = Path.home() / "claude"
44
+ BRAIN_DIR = CLAUDE_DIR / "brain"
45
+ COORD_DIR = CLAUDE_DIR / "coordination"
46
+ MEMORY_DIR = CLAUDE_DIR / "memory"
47
+ DAEMON_LOGS_DIR = CLAUDE_DIR / "daemon" / "logs"
49
48
 
50
49
  DAILY_SUMMARIES_DIR = BRAIN_DIR / "daily_summaries"
51
50
  SESSION_ARCHIVE_DIR = BRAIN_DIR / "session_archive"
@@ -55,7 +54,10 @@ HEARTBEAT_LOG = COORD_DIR / "heartbeat-log.json"
55
54
  REFLECTION_LOG = COORD_DIR / "reflection-log.json"
56
55
  SLEEP_LOG = COORD_DIR / "sleep-log.json"
57
56
 
58
- NEXO_DB = NEXO_HOME / "nexo.db"
57
+ MEMORY_MD = Path.home() / ".claude" / "projects" / f"-Users-{os.environ.get('USER', 'user')}" / "memory" / "MEMORY.md"
58
+ NEXO_DB = Path.home() / "claude" / "nexo-mcp" / "nexo.db"
59
+ CLAUDE_MEM_DB = Path.home() / ".claude-mem" / "claude-mem.db"
60
+ CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
59
61
 
60
62
  LAST_RUN_FILE = COORD_DIR / "sleep-last-run"
61
63
  LOCK_FILE = COORD_DIR / "sleep.lock"
@@ -290,7 +292,7 @@ def stage_a_cleanup() -> dict:
290
292
 
291
293
  # A8: Delete cortex/logs/*.log >7 days, truncate launchd logs >5MB
292
294
  cutoff_7 = TODAY - timedelta(days=7)
293
- cortex_logs = NEXO_HOME / "cortex" / "logs"
295
+ cortex_logs = Path.home() / "claude" / "cortex" / "logs"
294
296
  if cortex_logs.exists():
295
297
  for f in cortex_logs.glob("*.log"):
296
298
  if f.name.startswith("launchd-"):
@@ -474,12 +476,14 @@ def stage_c_learning_consolidation() -> dict:
474
476
  stats["potential_duplicates"] = duplicates
475
477
 
476
478
  # C4: Contradiction detection — NUNCA pairs in same category
477
- nunca_entries = [p for p in parsed if "nunca" in p["title"].lower() or "never" in p["title"].lower()]
479
+ nunca_entries = [p for p in parsed if "nunca" in p["title"].lower()]
478
480
  contradictions = []
479
481
  for nunca in nunca_entries:
480
482
  if len(contradictions) >= 5:
481
483
  break
482
- nunca_words_no_nunca = nunca["words"] - {"nunca", "never"}
484
+ # Look for same-category entries that don't contain NUNCA
485
+ # and whose remaining words overlap significantly (same subject, opposite stance)
486
+ nunca_words_no_nunca = nunca["words"] - {"nunca"}
483
487
  for other in parsed:
484
488
  if len(contradictions) >= 5:
485
489
  break
@@ -487,8 +491,9 @@ def stage_c_learning_consolidation() -> dict:
487
491
  continue
488
492
  if other["category"] != nunca["category"]:
489
493
  continue
490
- if "nunca" in other["title"].lower() or "never" in other["title"].lower():
494
+ if "nunca" in other["title"].lower():
491
495
  continue
496
+ # Check if they share meaningful subject words
492
497
  overlap = _word_overlap(nunca_words_no_nunca, other["words"])
493
498
  if overlap >= 0.50:
494
499
  contradictions.append({
@@ -518,11 +523,24 @@ def check_stage_b_conditions() -> dict:
518
523
  Returns dict with condition results and whether to trigger.
519
524
  """
520
525
  conditions = {
521
- "preferences_count": 0,
526
+ "memory_md_lines": 0,
527
+ "memory_md_over_limit": False,
528
+ "preferences_auto_sections": 0,
522
529
  "preferences_over_limit": False,
530
+ "claude_mem_old_observations": 0,
531
+ "claude_mem_over_limit": False,
523
532
  "should_trigger": False,
524
533
  }
525
534
 
535
+ # Check MEMORY.md line count
536
+ if MEMORY_MD.exists():
537
+ try:
538
+ lines = MEMORY_MD.read_text().splitlines()
539
+ conditions["memory_md_lines"] = len(lines)
540
+ conditions["memory_md_over_limit"] = len(lines) > 170
541
+ except Exception as e:
542
+ log(f"Stage B check: WARN reading MEMORY.md: {e}")
543
+
526
544
  # Check preferences count in SQLite
527
545
  if NEXO_DB.exists():
528
546
  try:
@@ -531,13 +549,16 @@ def check_stage_b_conditions() -> dict:
531
549
  cursor.execute("SELECT COUNT(*) FROM preferences")
532
550
  count = cursor.fetchone()[0]
533
551
  conn.close()
534
- conditions["preferences_count"] = count
552
+ conditions["preferences_auto_sections"] = count
535
553
  conditions["preferences_over_limit"] = count > 5
536
554
  except Exception as e:
537
555
  log(f"Stage B check: WARN reading nexo.db preferences: {e}")
538
556
 
557
+ # Check claude-mem.db observations >60 days
558
+ if CLAUDE_MEM_DB.exists():
539
559
  try:
540
560
  cutoff_epoch = int((datetime.now() - timedelta(days=60)).timestamp() * 1000)
561
+ conn = sqlite3.connect(str(CLAUDE_MEM_DB))
541
562
  cursor = conn.cursor()
542
563
  cursor.execute(
543
564
  "SELECT COUNT(*) FROM observations WHERE created_at_epoch < ?",
@@ -545,10 +566,15 @@ def check_stage_b_conditions() -> dict:
545
566
  )
546
567
  count = cursor.fetchone()[0]
547
568
  conn.close()
569
+ conditions["claude_mem_old_observations"] = count
570
+ conditions["claude_mem_over_limit"] = count > 500
548
571
  except Exception as e:
572
+ log(f"Stage B check: WARN reading claude-mem.db: {e}")
549
573
 
550
574
  conditions["should_trigger"] = (
551
- conditions["preferences_over_limit"]
575
+ conditions["memory_md_over_limit"]
576
+ or conditions["preferences_over_limit"]
577
+ or conditions["claude_mem_over_limit"]
552
578
  )
553
579
 
554
580
  return conditions
@@ -558,36 +584,54 @@ def build_stage_b_prompt(conditions: dict) -> str:
558
584
  """Build the prompt for Claude CLI based on which conditions triggered."""
559
585
  tasks = []
560
586
 
561
- if conditions["preferences_over_limit"]:
562
- tasks.append(f"""TASK: preferences in SQLite ({conditions['preferences_count']} records)
563
- DB: {NEXO_DB}, table: preferences (columns: key, value, category, updated_at)
564
- Connect with sqlite3. Delete duplicate preferences (same key) keeping the most recent.
565
- Delete preferences with updated_at older than 30 days if there is a newer duplicate.
566
- Report how many records were deleted.""")
587
+ if conditions["memory_md_over_limit"]:
588
+ tasks.append(f"""TAREA 1: MEMORY.md ({conditions['memory_md_lines']} lineas, limite 200)
589
+ Archivo: {MEMORY_MD}
590
+ Lee con Read tool, comprime incidentes resueltos >21 dias, fusiona duplicados, mantener <180 lineas.
591
+ PRESERVA toda la estructura de secciones existente. No elimines secciones enteras.""")
567
592
 
568
- Connect with sqlite3. Delete old low-value observations (discovery_tokens < 300 and >60 days old).
569
- Preserve anything marked CRITICAL, any credentials, tokens, API keys, or infrastructure details.
570
- Report how many records were deleted.""")
593
+ if conditions["preferences_over_limit"]:
594
+ tasks.append(f"""TAREA 2: preferences en SQLite ({conditions['preferences_auto_sections']} registros)
595
+ DB: {NEXO_DB}, tabla: preferences (columnas: key, value, category, updated_at)
596
+ Conecta con sqlite3. Elimina preferencias duplicadas (mismo key) manteniendo la mas reciente.
597
+ Elimina preferencias con updated_at mas antiguo de 30 dias si hay un duplicado mas reciente.
598
+ Reporta cuantos registros eliminaste.""")
599
+
600
+ if conditions["claude_mem_over_limit"]:
601
+ tasks.append(f"""TAREA 3: claude-mem observations ({conditions['claude_mem_old_observations']} registros >60d)
602
+ DB: {CLAUDE_MEM_DB}
603
+ Conecta con sqlite3. Ejecuta:
604
+ DELETE FROM observations WHERE created_at_epoch < {int((datetime.now() - timedelta(days=60)).timestamp() * 1000)}
605
+ AND discovery_tokens < 300
606
+ AND id NOT IN (SELECT id FROM observations WHERE
607
+ title LIKE '%CRITICO%' OR title LIKE '%MAXIMA%'
608
+ OR title LIKE '%credential%' OR title LIKE '%token%' OR title LIKE '%API%'
609
+ OR narrative LIKE '%CRITICO%' OR narrative LIKE '%MAXIMA%')
610
+ LIMIT 200;
611
+ Luego: DELETE FROM observations_fts WHERE rowid NOT IN (SELECT id FROM observations);
612
+ Luego: VACUUM;
613
+ Reporta cuantos registros eliminaste.""")
571
614
 
572
615
  if not tasks:
573
616
  return ""
574
617
 
575
618
  tasks_str = "\n\n".join(tasks)
576
619
 
577
- return f"""You are NEXO Sleep System. Your job is to PRUNE memory.
578
- NOT interactive. Do NOT wait for input. Execute the following tasks and exit.
620
+ return f"""Eres NEXO Sleep System. Tu trabajo es PODAR la memoria.
621
+ NO eres interactivo. NO esperas input. Ejecuta las siguientes tareas y sal.
579
622
 
580
- ABSOLUTE RULES:
581
- - NEVER delete credentials, tokens, account IDs, API endpoints, keys, secrets.
582
- - NEVER delete operational rules marked as "CRITICAL".
583
- - YES to merging redundant sections.
584
- - YES to deleting obsolete technical info (fixed >30 days ago and never referenced after).
585
- - YES to compressing long paragraphs into concise bullets.
586
- - Every line you delete must have a clear reason. When in doubt, do NOT delete.
623
+ REGLAS ABSOLUTAS:
624
+ - NUNCA borres credenciales, tokens, IDs de cuentas, API endpoints, claves, secrets.
625
+ - NUNCA borres reglas operativas marcadas como "CRITICO" o "MAXIMA PRIORIDAD".
626
+ - NUNCA borres informacion sobre infraestructura (servidores, repos, deploys).
627
+ - SI puedes fusionar secciones redundantes.
628
+ - SI puedes eliminar informacion tecnica obsoleta (arreglada hace >30 dias y nunca referenciada despues).
629
+ - SI puedes comprimir parrafos largos en bullets concisos.
630
+ - Cada linea que elimines debe tener una razon clara. En caso de duda, NO borres.
587
631
 
588
632
  {tasks_str}
589
633
 
590
- When done, print a JSON summary of actions taken."""
634
+ Al terminar, imprime un resumen JSON con las acciones realizadas."""
591
635
 
592
636
 
593
637
  def run_stage_b(conditions: dict) -> dict:
@@ -596,6 +640,8 @@ def run_stage_b(conditions: dict) -> dict:
596
640
  if not prompt:
597
641
  return {"skipped": True, "reason": "No tasks to run"}
598
642
 
643
+ if not CLAUDE_CLI.exists():
644
+ return {"error": f"Claude CLI not found at {CLAUDE_CLI}"}
599
645
 
600
646
  log("Stage B: Invoking Claude CLI (sonnet)...")
601
647
 
@@ -606,9 +652,10 @@ def run_stage_b(conditions: dict) -> dict:
606
652
  env.pop("CLAUDE_CODE", None)
607
653
 
608
654
  result = subprocess.run(
655
+ [str(CLAUDE_CLI), "-p", prompt, "--model", "sonnet"],
609
656
  capture_output=True,
610
657
  text=True,
611
- timeout=300,
658
+ timeout=600,
612
659
  env=env
613
660
  )
614
661
 
@@ -633,7 +680,7 @@ def run_stage_b(conditions: dict) -> dict:
633
680
  }
634
681
 
635
682
  except subprocess.TimeoutExpired:
636
- log("Stage B: Claude CLI timed out (300s)")
683
+ log("Stage B: Claude CLI timed out (600s)")
637
684
  return {"error": "timeout"}
638
685
  except Exception as e:
639
686
  log(f"Stage B: Exception: {e}")
@@ -710,8 +757,12 @@ def main():
710
757
  conditions = check_stage_b_conditions()
711
758
  run_log["stage_b_conditions"] = conditions
712
759
 
713
- log(f" nexo.db preferences: {conditions['preferences_count']} rows "
760
+ log(f" MEMORY.md: {conditions['memory_md_lines']} lines "
761
+ f"(trigger={conditions['memory_md_over_limit']})")
762
+ log(f" nexo.db preferences: {conditions['preferences_auto_sections']} rows "
714
763
  f"(trigger={conditions['preferences_over_limit']})")
764
+ log(f" claude-mem old observations: {conditions['claude_mem_old_observations']} "
765
+ f"(trigger={conditions['claude_mem_over_limit']})")
715
766
 
716
767
  if conditions["should_trigger"]:
717
768
  log("Stage B: Conditions met, running intelligent pruning...")
@@ -731,7 +782,7 @@ def main():
731
782
  # Register successful run for catch-up
732
783
  try:
733
784
  import json as _json
734
- _state_file = NEXO_HOME / "operations" / ".catchup-state.json"
785
+ _state_file = Path.home() / "claude" / "operations" / ".catchup-state.json"
735
786
  _state = _json.loads(_state_file.read_text()) if _state_file.exists() else {}
736
787
  _state["sleep"] = datetime.now().isoformat()
737
788
  _state_file.write_text(_json.dumps(_state, indent=2))