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.
- package/README.md +158 -72
- package/bin/nexo-brain 2.js +610 -0
- package/package.json +2 -2
- package/scripts/pre-commit-check 2.sh +55 -0
- package/src/cognitive.py +1582 -56
- package/src/db.py +49 -25
- package/src/hooks/auto_capture.py +208 -0
- package/src/plugins/cognitive_memory.py +276 -17
- package/src/scripts/nexo-catchup.py +32 -15
- package/src/scripts/nexo-cognitive-decay.py +2 -4
- package/src/scripts/nexo-daily-self-audit.py +148 -29
- package/src/scripts/nexo-immune.py +869 -0
- package/src/scripts/nexo-postmortem-consolidator.py +42 -40
- package/src/scripts/nexo-sleep.py +90 -39
- package/src/scripts/nexo-synthesis.py +78 -76
- package/src/tools_sessions.py +2 -2
- package/templates/CLAUDE.md 2.template +89 -0
- package/templates/CLAUDE.md.template +1 -1
|
@@ -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
|
-
|
|
30
|
-
sys.path.insert(0, str(
|
|
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 =
|
|
34
|
-
SESSION_BUFFER =
|
|
35
|
-
MEMORY_DIR =
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
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("
|
|
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:
|
|
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:**
|
|
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:**
|
|
170
|
+
**How to apply:** Verificar esta regla al inicio de cada sesión y antes de presentar trabajo como completado.
|
|
170
171
|
|
|
171
|
-
**
|
|
172
|
+
**Evidencia (autocríticas originales):**
|
|
172
173
|
"""
|
|
173
174
|
for i, critique in enumerate(source_critiques[:3], 1):
|
|
174
|
-
content += f"-
|
|
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
|
|
274
|
+
# Ingest into STM as sensory
|
|
275
|
+
# Customize domain keywords for your own projects
|
|
274
276
|
domain = ""
|
|
275
|
-
|
|
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 =
|
|
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,
|
|
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("
|
|
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"
|
|
491
|
-
"content": f"
|
|
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"
|
|
516
|
-
"content": f"
|
|
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"
|
|
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 =
|
|
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"
|
|
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("##
|
|
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("##
|
|
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("##
|
|
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 =
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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 =
|
|
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()
|
|
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
|
-
|
|
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()
|
|
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
|
-
"
|
|
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["
|
|
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["
|
|
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["
|
|
562
|
-
tasks.append(f"""
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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"""
|
|
578
|
-
|
|
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
|
-
|
|
581
|
-
-
|
|
582
|
-
-
|
|
583
|
-
-
|
|
584
|
-
-
|
|
585
|
-
-
|
|
586
|
-
-
|
|
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
|
-
|
|
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=
|
|
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 (
|
|
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"
|
|
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 =
|
|
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))
|