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
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
"""Cognitive Memory plugin — RAG retrieval over NEXO's Atkinson-Shiffrin memory stores."""
|
|
2
2
|
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
# Ensure site-packages is in path for numpy/fastembed
|
|
7
|
+
_site = "/opt/homebrew/lib/python{}.{}/site-packages".format(sys.version_info.major, sys.version_info.minor)
|
|
8
|
+
if os.path.isdir(_site) and _site not in sys.path:
|
|
9
|
+
sys.path.insert(0, _site)
|
|
10
|
+
|
|
3
11
|
import cognitive
|
|
4
12
|
|
|
5
13
|
|
|
@@ -10,6 +18,9 @@ def handle_cognitive_retrieve(
|
|
|
10
18
|
stores: str = "both",
|
|
11
19
|
source_type: str = "",
|
|
12
20
|
domain: str = "",
|
|
21
|
+
include_archived: bool = False,
|
|
22
|
+
use_hyde: bool = False,
|
|
23
|
+
spreading_depth: int = 0,
|
|
13
24
|
) -> str:
|
|
14
25
|
"""RAG query over cognitive memory (STM + LTM). Triggers rehearsal on retrieved memories.
|
|
15
26
|
|
|
@@ -19,7 +30,10 @@ def handle_cognitive_retrieve(
|
|
|
19
30
|
min_score: Minimum cosine similarity score (default 0.5)
|
|
20
31
|
stores: Which store to search — "both", "stm", or "ltm" (default "both")
|
|
21
32
|
source_type: Filter by source type e.g. "change", "learning", "diary" (default: all)
|
|
22
|
-
domain: Filter by domain e.g. "
|
|
33
|
+
domain: Filter by domain e.g. "wazion", "shopify" (default: all)
|
|
34
|
+
include_archived: If True, also search archived memories (default False)
|
|
35
|
+
use_hyde: If True, use HyDE query expansion — embeds 3-5 query variants and searches with centroid. Better recall for conceptual queries. (default False)
|
|
36
|
+
spreading_depth: If >0, boost co-activated neighbors (memories frequently retrieved together). 1=direct neighbors only. (default 0)
|
|
23
37
|
"""
|
|
24
38
|
if not query or not query.strip():
|
|
25
39
|
return "ERROR: query is required."
|
|
@@ -32,6 +46,9 @@ def handle_cognitive_retrieve(
|
|
|
32
46
|
exclude_dormant=True,
|
|
33
47
|
rehearse=True,
|
|
34
48
|
source_type_filter=source_type,
|
|
49
|
+
include_archived=include_archived,
|
|
50
|
+
use_hyde=use_hyde,
|
|
51
|
+
spreading_depth=spreading_depth,
|
|
35
52
|
)
|
|
36
53
|
|
|
37
54
|
# Apply domain filter post-search (cognitive.search doesn't filter by domain natively)
|
|
@@ -39,7 +56,12 @@ def handle_cognitive_retrieve(
|
|
|
39
56
|
results = [r for r in results if r.get("domain", "") == domain]
|
|
40
57
|
|
|
41
58
|
formatted = cognitive.format_results(results)
|
|
42
|
-
|
|
59
|
+
mode_parts = [f"stores={stores}", f"min_score={min_score}"]
|
|
60
|
+
if use_hyde:
|
|
61
|
+
mode_parts.append("hyde=ON")
|
|
62
|
+
if spreading_depth > 0:
|
|
63
|
+
mode_parts.append(f"spreading={spreading_depth}")
|
|
64
|
+
header = f"COGNITIVE RETRIEVE — query: '{query}' | {len(results)} results ({', '.join(mode_parts)})\n\n"
|
|
43
65
|
return header + formatted
|
|
44
66
|
|
|
45
67
|
|
|
@@ -68,6 +90,21 @@ def handle_cognitive_stats() -> str:
|
|
|
68
90
|
for domain, cnt in stats["top_domains_ltm"]:
|
|
69
91
|
lines.append(f" {domain}: {cnt}")
|
|
70
92
|
|
|
93
|
+
if "quarantine" in stats:
|
|
94
|
+
q = stats["quarantine"]
|
|
95
|
+
lines.append(f" Quarantine pending: {q.get('pending', 0)}")
|
|
96
|
+
lines.append(f" Quarantine promoted: {q.get('promoted', 0)}")
|
|
97
|
+
lines.append(f" Quarantine rejected: {q.get('rejected', 0)}")
|
|
98
|
+
lines.append(f" Quarantine expired: {q.get('expired', 0)}")
|
|
99
|
+
|
|
100
|
+
if "prediction_error_gate" in stats:
|
|
101
|
+
g = stats["prediction_error_gate"]
|
|
102
|
+
lines.append(" PE Gate (session):")
|
|
103
|
+
lines.append(f" Accepted (novel): {g['accepted_novel']}")
|
|
104
|
+
lines.append(f" Accepted (refine): {g['accepted_refinement']}")
|
|
105
|
+
lines.append(f" Rejected (redundant): {g['rejected']}")
|
|
106
|
+
lines.append(f" Rejection rate: {g['rejection_rate_pct']}%")
|
|
107
|
+
|
|
71
108
|
return "\n".join(lines)
|
|
72
109
|
|
|
73
110
|
|
|
@@ -104,6 +141,12 @@ def handle_cognitive_inspect(memory_id: int, store: str = "ltm") -> str:
|
|
|
104
141
|
f" last_accessed: {row['last_accessed']}",
|
|
105
142
|
]
|
|
106
143
|
|
|
144
|
+
# Lifecycle state
|
|
145
|
+
lifecycle = row["lifecycle_state"] or "active"
|
|
146
|
+
lines.append(f" lifecycle: {lifecycle}")
|
|
147
|
+
if row["snooze_until"]:
|
|
148
|
+
lines.append(f" snooze_until: {row['snooze_until']}")
|
|
149
|
+
|
|
107
150
|
if store == "ltm":
|
|
108
151
|
dormant_label = "YES" if row["is_dormant"] else "no"
|
|
109
152
|
lines.append(f" dormant: {dormant_label}")
|
|
@@ -120,7 +163,7 @@ def handle_cognitive_inspect(memory_id: int, store: str = "ltm") -> str:
|
|
|
120
163
|
|
|
121
164
|
|
|
122
165
|
def handle_cognitive_metrics(days: int = 7) -> str:
|
|
123
|
-
"""Cognitive memory performance metrics.
|
|
166
|
+
"""Cognitive memory performance metrics (spec section 9).
|
|
124
167
|
|
|
125
168
|
Returns retrieval relevance %, repeat error rate, score distribution,
|
|
126
169
|
and whether multilingual model switch is recommended.
|
|
@@ -155,7 +198,7 @@ def handle_cognitive_metrics(days: int = 7) -> str:
|
|
|
155
198
|
|
|
156
199
|
if metrics["needs_multilingual"]:
|
|
157
200
|
lines.append("")
|
|
158
|
-
lines.append("RECOMMENDATION: Switch to multilingual model (intfloat/multilingual-e5-small)")
|
|
201
|
+
lines.append("⚠ RECOMMENDATION: Switch to multilingual model (intfloat/multilingual-e5-small)")
|
|
159
202
|
lines.append(f" Reason: relevance {metrics['retrieval_relevance_pct']}% < 70% with {metrics['total_retrievals']}+ retrievals")
|
|
160
203
|
|
|
161
204
|
if repeats["duplicates"]:
|
|
@@ -165,17 +208,27 @@ def handle_cognitive_metrics(days: int = 7) -> str:
|
|
|
165
208
|
lines.append(f" [{d['score']}] STM#{d['new_stm_id']}: {d['new_content'][:60]}...")
|
|
166
209
|
lines.append(f" ≈ LTM#{d['ltm_id']}: {d['ltm_content'][:60]}...")
|
|
167
210
|
|
|
211
|
+
# Prediction Error Gate stats
|
|
212
|
+
gate = cognitive.get_gate_stats()
|
|
213
|
+
if gate["total_evaluated"] > 0:
|
|
214
|
+
lines.append("")
|
|
215
|
+
lines.append("Prediction Error Gate (session):")
|
|
216
|
+
lines.append(f" Novel accepted: {gate['accepted_novel']}")
|
|
217
|
+
lines.append(f" Refinements: {gate['accepted_refinement']}")
|
|
218
|
+
lines.append(f" Rejected redundant: {gate['rejected']}")
|
|
219
|
+
lines.append(f" Rejection rate: {gate['rejection_rate_pct']}%")
|
|
220
|
+
|
|
168
221
|
return "\n".join(lines)
|
|
169
222
|
|
|
170
223
|
|
|
171
224
|
def handle_cognitive_sentiment(text: str) -> str:
|
|
172
|
-
"""Detect user sentiment from
|
|
225
|
+
"""Detect the user's sentiment from his text. Returns mood, intensity, and guidance.
|
|
173
226
|
|
|
174
227
|
Call this with the user's recent message to adapt NEXO's tone and behavior.
|
|
175
228
|
Also logs the sentiment for historical tracking.
|
|
176
229
|
|
|
177
230
|
Args:
|
|
178
|
-
text:
|
|
231
|
+
text: the user's recent message or instruction
|
|
179
232
|
"""
|
|
180
233
|
result = cognitive.log_sentiment(text)
|
|
181
234
|
trust = cognitive.get_trust_score()
|
|
@@ -241,27 +294,27 @@ def handle_cognitive_trust(event: str = '', context: str = '', delta: float = No
|
|
|
241
294
|
def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
|
|
242
295
|
"""Detect cognitive dissonance: find established memories that conflict with a new instruction.
|
|
243
296
|
|
|
244
|
-
Use BEFORE applying a new preference or rule that might contradict
|
|
245
|
-
If conflicts found, verbalize them and ask to resolve.
|
|
297
|
+
Use BEFORE applying a new preference or rule from the user that might contradict
|
|
298
|
+
existing knowledge. If conflicts found, verbalize them and ask the user to resolve.
|
|
246
299
|
|
|
247
300
|
Args:
|
|
248
301
|
instruction: The new instruction or preference to check against LTM
|
|
249
302
|
force: If True, skip discussion — execute instruction, auto-resolve all conflicts as
|
|
250
|
-
'exception', and flag for review.
|
|
303
|
+
'exception', and flag for review in the nocturnal process (23:30).
|
|
251
304
|
"""
|
|
252
305
|
conflicts = cognitive.detect_dissonance(instruction)
|
|
253
306
|
if not conflicts:
|
|
254
307
|
return f"No dissonance detected. Instruction '{instruction[:80]}' is consistent with existing LTM."
|
|
255
308
|
|
|
256
309
|
if force:
|
|
257
|
-
# Auto-resolve all as exceptions
|
|
310
|
+
# Auto-resolve all as exceptions, log for nocturnal review
|
|
258
311
|
for c in conflicts:
|
|
259
312
|
cognitive.resolve_dissonance(
|
|
260
313
|
c["memory_id"], "exception",
|
|
261
|
-
f"[FORCE] {instruction[:200]} — auto-exception, pending review"
|
|
314
|
+
f"[FORCE] {instruction[:200]} — auto-exception, pending nocturnal review"
|
|
262
315
|
)
|
|
263
316
|
return (f"FORCE: {len(conflicts)} conflicts auto-resolved as exceptions. "
|
|
264
|
-
f"Instruction executed. Flagged for review.")
|
|
317
|
+
f"Instruction executed. Flagged for review at 23:30.")
|
|
265
318
|
|
|
266
319
|
lines = [
|
|
267
320
|
f"COGNITIVE DISSONANCE DETECTED — {len(conflicts)} conflicting memories:",
|
|
@@ -275,7 +328,7 @@ def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
|
|
|
275
328
|
lines.append("")
|
|
276
329
|
|
|
277
330
|
lines.append("RESOLVE with nexo_cognitive_resolve, or use force=True to skip:")
|
|
278
|
-
lines.append(" - 'paradigm_shift':
|
|
331
|
+
lines.append(" - 'paradigm_shift': the user changed his mind permanently.")
|
|
279
332
|
lines.append(" - 'exception': One-time override. Old memory stays.")
|
|
280
333
|
lines.append(" - 'override': Old memory was wrong.")
|
|
281
334
|
|
|
@@ -283,7 +336,7 @@ def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
|
|
|
283
336
|
|
|
284
337
|
|
|
285
338
|
def handle_cognitive_resolve(memory_id: int, resolution: str, context: str = '') -> str:
|
|
286
|
-
"""Resolve a cognitive dissonance.
|
|
339
|
+
"""Resolve a cognitive dissonance by applying the user's decision.
|
|
287
340
|
|
|
288
341
|
Args:
|
|
289
342
|
memory_id: The LTM memory ID from the dissonance detection
|
|
@@ -293,13 +346,219 @@ def handle_cognitive_resolve(memory_id: int, resolution: str, context: str = '')
|
|
|
293
346
|
return cognitive.resolve_dissonance(memory_id, resolution, context)
|
|
294
347
|
|
|
295
348
|
|
|
349
|
+
def handle_cognitive_pin(memory_id: int, store: str = "auto") -> str:
|
|
350
|
+
"""Pin a memory so it NEVER decays and gets boosted in search results (+0.2 similarity).
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
memory_id: Integer ID of the memory to pin
|
|
354
|
+
store: Which store — "stm", "ltm", or "auto" (tries both, default "auto")
|
|
355
|
+
"""
|
|
356
|
+
return cognitive.set_lifecycle(memory_id, "pinned", store)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def handle_cognitive_snooze(memory_id: int, until_date: str, store: str = "auto") -> str:
|
|
360
|
+
"""Snooze a memory — hidden from searches until the given date, then auto-restores to active.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
memory_id: Integer ID of the memory to snooze
|
|
364
|
+
until_date: Date to restore the memory (YYYY-MM-DD format)
|
|
365
|
+
store: Which store — "stm", "ltm", or "auto" (tries both, default "auto")
|
|
366
|
+
"""
|
|
367
|
+
return cognitive.set_lifecycle(memory_id, "snoozed", store, snooze_until=until_date)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def handle_cognitive_archive(memory_id: int, store: str = "auto") -> str:
|
|
371
|
+
"""Archive a memory — stored but excluded from normal searches. Can be restored later.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
memory_id: Integer ID of the memory to archive
|
|
375
|
+
store: Which store — "stm", "ltm", or "auto" (tries both, default "auto")
|
|
376
|
+
"""
|
|
377
|
+
return cognitive.set_lifecycle(memory_id, "archived", store)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def handle_cognitive_restore(memory_id: int, store: str = "auto") -> str:
|
|
381
|
+
"""Restore a memory to active state (from pinned, snoozed, or archived).
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
memory_id: Integer ID of the memory to restore
|
|
385
|
+
store: Which store — "stm", "ltm", or "auto" (tries both, default "auto")
|
|
386
|
+
"""
|
|
387
|
+
return cognitive.set_lifecycle(memory_id, "active", store)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def handle_cognitive_quarantine_list(status: str = "pending", limit: int = 20) -> str:
|
|
391
|
+
"""List quarantine queue items. Shows memories awaiting promotion to STM.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
status: Filter — 'pending', 'promoted', 'rejected', 'expired', or 'all' (default 'pending')
|
|
395
|
+
limit: Max items to return (default 20)
|
|
396
|
+
"""
|
|
397
|
+
items = cognitive.quarantine_list(status=status, limit=limit)
|
|
398
|
+
stats = cognitive.quarantine_stats()
|
|
399
|
+
|
|
400
|
+
lines = [
|
|
401
|
+
f"QUARANTINE QUEUE — {stats['pending']} pending | {stats['promoted']} promoted | {stats['rejected']} rejected | {stats['expired']} expired",
|
|
402
|
+
f"Showing: {status} (limit {limit})",
|
|
403
|
+
"",
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
if not items:
|
|
407
|
+
lines.append("No items found.")
|
|
408
|
+
else:
|
|
409
|
+
for item in items:
|
|
410
|
+
lines.append(f" #{item['id']} [{item['status']}] source={item['source']} type={item['source_type']} domain={item['domain'] or '-'}")
|
|
411
|
+
lines.append(f" confidence={item['confidence']:.1f} checks={item['promotion_checks']} created={item['created_at'][:16]}")
|
|
412
|
+
if item['promoted_at']:
|
|
413
|
+
lines.append(f" promoted_at={item['promoted_at'][:16]}")
|
|
414
|
+
lines.append(f" {item['content']}")
|
|
415
|
+
lines.append("")
|
|
416
|
+
|
|
417
|
+
return "\n".join(lines)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def handle_cognitive_quarantine_promote(quarantine_id: int) -> str:
|
|
421
|
+
"""Manually promote a quarantine item to STM, bypassing the automatic promotion policy.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
quarantine_id: ID of the quarantine entry to promote
|
|
425
|
+
"""
|
|
426
|
+
return cognitive.quarantine_promote(quarantine_id)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def handle_cognitive_quarantine_reject(quarantine_id: int, reason: str = "") -> str:
|
|
430
|
+
"""Manually reject a quarantine item.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
quarantine_id: ID of the quarantine entry to reject
|
|
434
|
+
reason: Optional reason for rejection
|
|
435
|
+
"""
|
|
436
|
+
return cognitive.quarantine_reject(quarantine_id, reason)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def handle_cognitive_quarantine_process() -> str:
|
|
440
|
+
"""Run the quarantine promotion cycle. Evaluates all pending items against the promotion policy.
|
|
441
|
+
|
|
442
|
+
Promotion rules:
|
|
443
|
+
- source='user_direct' → already promoted at ingest
|
|
444
|
+
- source='inferred' + second occurrence found → promote
|
|
445
|
+
- source='agent_observation' + >24h old + no LTM contradiction → promote
|
|
446
|
+
- Contradicts LTM (cosine >0.8) → reject
|
|
447
|
+
- >7 days old → expire
|
|
448
|
+
"""
|
|
449
|
+
result = cognitive.process_quarantine()
|
|
450
|
+
lines = [
|
|
451
|
+
"QUARANTINE PROCESSING COMPLETE",
|
|
452
|
+
f" Promoted: {result['promoted']}",
|
|
453
|
+
f" Rejected: {result['rejected']}",
|
|
454
|
+
f" Expired: {result['expired']}",
|
|
455
|
+
f" Still pending: {result['still_pending']}",
|
|
456
|
+
f" Total: {result['total_processed']}",
|
|
457
|
+
]
|
|
458
|
+
return "\n".join(lines)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
# ============================================================================
|
|
462
|
+
# Prospective Memory trigger handlers (Feature 3)
|
|
463
|
+
# ============================================================================
|
|
464
|
+
|
|
465
|
+
def handle_cognitive_trigger_create(pattern: str, action: str, context: str = "") -> str:
|
|
466
|
+
"""Create a prospective memory trigger — fires when text matches pattern.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
pattern: Keywords to match (case-insensitive, comma-separated for OR matching)
|
|
470
|
+
action: What to do / remind about when the trigger fires
|
|
471
|
+
context: Optional context about why this trigger was created
|
|
472
|
+
"""
|
|
473
|
+
trigger_id = cognitive.create_trigger(pattern, action, context)
|
|
474
|
+
return f"Trigger #{trigger_id} created — armed. Pattern: '{pattern}' | Action: '{action}'"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def handle_cognitive_trigger_list(status: str = "armed") -> str:
|
|
478
|
+
"""List prospective memory triggers.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
status: Filter — 'armed' (active, waiting), 'fired' (already triggered), 'all'
|
|
482
|
+
"""
|
|
483
|
+
triggers = cognitive.list_triggers(status)
|
|
484
|
+
if not triggers:
|
|
485
|
+
return f"No {status} triggers found."
|
|
486
|
+
|
|
487
|
+
lines = [f"PROSPECTIVE TRIGGERS ({status}) — {len(triggers)} total", ""]
|
|
488
|
+
for t in triggers:
|
|
489
|
+
status_icon = "+" if t["status"] == "armed" else "x"
|
|
490
|
+
lines.append(f" [{status_icon}] #{t['id']} pattern='{t['trigger_pattern']}'")
|
|
491
|
+
lines.append(f" action: {t['action']}")
|
|
492
|
+
if t.get("context"):
|
|
493
|
+
lines.append(f" context: {t['context']}")
|
|
494
|
+
lines.append(f" created: {t['created_at'][:16]}")
|
|
495
|
+
if t.get("fired_at"):
|
|
496
|
+
lines.append(f" fired: {t['fired_at'][:16]}")
|
|
497
|
+
lines.append("")
|
|
498
|
+
|
|
499
|
+
return "\n".join(lines)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def handle_cognitive_trigger_check(text: str, use_semantic: bool = False) -> str:
|
|
503
|
+
"""Check text against all armed triggers and fire matching ones.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
text: Text to check against triggers (e.g. user message, heartbeat context)
|
|
507
|
+
use_semantic: Also use embedding similarity (slower but catches conceptual matches)
|
|
508
|
+
"""
|
|
509
|
+
fired = cognitive.check_triggers(text, use_semantic=use_semantic)
|
|
510
|
+
if not fired:
|
|
511
|
+
return "No triggers fired."
|
|
512
|
+
|
|
513
|
+
lines = [f"TRIGGERS FIRED: {len(fired)}", ""]
|
|
514
|
+
for t in fired:
|
|
515
|
+
lines.append(f" #{t['id']} [{t['match_type']}] pattern='{t['pattern']}'")
|
|
516
|
+
lines.append(f" ACTION: {t['action']}")
|
|
517
|
+
if t.get("context"):
|
|
518
|
+
lines.append(f" context: {t['context']}")
|
|
519
|
+
lines.append("")
|
|
520
|
+
|
|
521
|
+
return "\n".join(lines)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def handle_cognitive_trigger_delete(trigger_id: int) -> str:
|
|
525
|
+
"""Delete a prospective memory trigger.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
trigger_id: ID of the trigger to delete
|
|
529
|
+
"""
|
|
530
|
+
return cognitive.delete_trigger(trigger_id)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def handle_cognitive_trigger_rearm(trigger_id: int) -> str:
|
|
534
|
+
"""Re-arm a fired trigger so it can fire again.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
trigger_id: ID of the trigger to re-arm
|
|
538
|
+
"""
|
|
539
|
+
return cognitive.rearm_trigger(trigger_id)
|
|
540
|
+
|
|
541
|
+
|
|
296
542
|
TOOLS = [
|
|
297
543
|
(handle_cognitive_retrieve, "nexo_cognitive_retrieve", "RAG query over cognitive memory (STM+LTM). Triggers rehearsal on retrieved results."),
|
|
298
|
-
(handle_cognitive_stats, "nexo_cognitive_stats", "Cognitive memory system metrics: STM/LTM counts, strengths, retrieval stats"),
|
|
544
|
+
(handle_cognitive_stats, "nexo_cognitive_stats", "Cognitive memory system metrics: STM/LTM counts, strengths, retrieval stats, quarantine counts"),
|
|
299
545
|
(handle_cognitive_inspect, "nexo_cognitive_inspect", "Inspect a specific memory by ID (debug). Does NOT trigger rehearsal."),
|
|
300
|
-
(handle_cognitive_metrics, "nexo_cognitive_metrics", "Performance metrics: retrieval relevance %, repeat error rate, multilingual recommendation"),
|
|
546
|
+
(handle_cognitive_metrics, "nexo_cognitive_metrics", "Performance metrics: retrieval relevance %, repeat error rate, multilingual recommendation (spec section 9)"),
|
|
301
547
|
(handle_cognitive_dissonance, "nexo_cognitive_dissonance", "Detect conflicts between a new instruction and established LTM memories. force=True to skip discussion."),
|
|
302
548
|
(handle_cognitive_resolve, "nexo_cognitive_resolve", "Resolve a cognitive dissonance: paradigm_shift, exception, or override."),
|
|
303
|
-
(handle_cognitive_sentiment, "nexo_cognitive_sentiment", "Detect user sentiment and get tone guidance. Also logs for tracking."),
|
|
549
|
+
(handle_cognitive_sentiment, "nexo_cognitive_sentiment", "Detect the user's sentiment and get tone guidance. Also logs for tracking."),
|
|
304
550
|
(handle_cognitive_trust, "nexo_cognitive_trust", "View or adjust trust score (0-100). Without args: view. With event: adjust."),
|
|
551
|
+
(handle_cognitive_pin, "nexo_cognitive_pin", "Pin a memory — never decays, boosted +0.2 in search results."),
|
|
552
|
+
(handle_cognitive_snooze, "nexo_cognitive_snooze", "Snooze a memory — hidden from searches until a date, then auto-restores."),
|
|
553
|
+
(handle_cognitive_archive, "nexo_cognitive_archive", "Archive a memory — excluded from searches, can be restored."),
|
|
554
|
+
(handle_cognitive_restore, "nexo_cognitive_restore", "Restore a memory to active state (from pinned/snoozed/archived)."),
|
|
555
|
+
(handle_cognitive_quarantine_list, "nexo_cognitive_quarantine_list", "List quarantine queue items awaiting promotion to STM."),
|
|
556
|
+
(handle_cognitive_quarantine_promote, "nexo_cognitive_quarantine_promote", "Manually promote a quarantine item to STM."),
|
|
557
|
+
(handle_cognitive_quarantine_reject, "nexo_cognitive_quarantine_reject", "Manually reject a quarantine item."),
|
|
558
|
+
(handle_cognitive_quarantine_process, "nexo_cognitive_quarantine_process", "Run quarantine promotion cycle — evaluate pending items against policy."),
|
|
559
|
+
(handle_cognitive_trigger_create, "nexo_cognitive_trigger_create", "Create a prospective memory trigger — 'when X is mentioned, remind about Y'."),
|
|
560
|
+
(handle_cognitive_trigger_list, "nexo_cognitive_trigger_list", "List prospective triggers by status (armed/fired/all)."),
|
|
561
|
+
(handle_cognitive_trigger_check, "nexo_cognitive_trigger_check", "Check text against armed triggers. Returns fired triggers with actions."),
|
|
562
|
+
(handle_cognitive_trigger_delete, "nexo_cognitive_trigger_delete", "Delete a prospective trigger by ID."),
|
|
563
|
+
(handle_cognitive_trigger_rearm, "nexo_cognitive_trigger_rearm", "Re-arm a fired trigger so it can fire again."),
|
|
305
564
|
]
|
|
@@ -8,6 +8,7 @@ runs them in the correct order.
|
|
|
8
8
|
|
|
9
9
|
Scheduled tasks (ordered by intended run time):
|
|
10
10
|
03:00 — cognitive-decay (Ebbinghaus decay + STM→LTM promotion)
|
|
11
|
+
03:00 — evolution (weekly, Sundays only)
|
|
11
12
|
04:00 — sleep (session cleanup)
|
|
12
13
|
07:00 — self-audit (health checks + weekly cognitive GC on Sundays)
|
|
13
14
|
23:30 — postmortem (consolidation + sensory register)
|
|
@@ -23,14 +24,15 @@ import sys
|
|
|
23
24
|
from datetime import datetime, timedelta
|
|
24
25
|
from pathlib import Path
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
LOG_DIR =
|
|
27
|
+
HOME = Path.home()
|
|
28
|
+
LOG_DIR = HOME / "claude" / "logs"
|
|
28
29
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
29
30
|
LOG_FILE = LOG_DIR / "catchup.log"
|
|
30
|
-
STATE_FILE =
|
|
31
|
-
SCRIPTS = NEXO_HOME / "src" / "scripts"
|
|
31
|
+
STATE_FILE = HOME / "claude" / "operations" / ".catchup-state.json"
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
PYTHON_BREW = "/opt/homebrew/bin/python3"
|
|
34
|
+
PYTHON_SYS = "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3"
|
|
35
|
+
SCRIPTS = HOME / "claude" / "scripts"
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
def log(msg: str):
|
|
@@ -93,7 +95,7 @@ def should_run(task_name: str, hour: int, minute: int, state: dict, weekday: int
|
|
|
93
95
|
return last_run < last_scheduled
|
|
94
96
|
|
|
95
97
|
|
|
96
|
-
def run_task(name: str, script: str, state: dict) -> bool:
|
|
98
|
+
def run_task(name: str, python: str, script: str, state: dict) -> bool:
|
|
97
99
|
"""Execute a task and update state."""
|
|
98
100
|
script_path = str(SCRIPTS / script)
|
|
99
101
|
if not Path(script_path).exists():
|
|
@@ -103,9 +105,9 @@ def run_task(name: str, script: str, state: dict) -> bool:
|
|
|
103
105
|
log(f" RUNNING {name}: {script}")
|
|
104
106
|
try:
|
|
105
107
|
result = subprocess.run(
|
|
106
|
-
[
|
|
108
|
+
[python, script_path],
|
|
107
109
|
capture_output=True, text=True, timeout=300,
|
|
108
|
-
env={**os.environ, "HOME": str(
|
|
110
|
+
env={**os.environ, "HOME": str(HOME), "NEXO_CATCHUP": "1"}
|
|
109
111
|
)
|
|
110
112
|
if result.returncode == 0:
|
|
111
113
|
log(f" OK {name} (exit 0)")
|
|
@@ -129,20 +131,35 @@ def main():
|
|
|
129
131
|
state = load_state()
|
|
130
132
|
|
|
131
133
|
# Define tasks in execution order (matching their intended schedule order)
|
|
134
|
+
# Auto-update check FIRST
|
|
135
|
+
update_script = SCRIPTS / "nexo-auto-update.py"
|
|
136
|
+
if update_script.exists():
|
|
137
|
+
log("Checking for NEXO updates...")
|
|
138
|
+
try:
|
|
139
|
+
subprocess.run(
|
|
140
|
+
[PYTHON_BREW if os.path.exists(PYTHON_BREW) else PYTHON_SYS, str(update_script)],
|
|
141
|
+
capture_output=True, text=True, timeout=60,
|
|
142
|
+
env={**os.environ, "HOME": str(HOME), "NEXO_HOME": str(HOME / "claude" / "nexo-mcp")}
|
|
143
|
+
)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
log(f" Update check failed: {e}")
|
|
146
|
+
|
|
132
147
|
tasks = [
|
|
133
|
-
# (name, hour, minute, script, weekday)
|
|
134
|
-
("cognitive-decay", 3, 0, "nexo-cognitive-decay.py", None),
|
|
135
|
-
("
|
|
136
|
-
("
|
|
137
|
-
("
|
|
148
|
+
# (name, hour, minute, python, script, weekday)
|
|
149
|
+
("cognitive-decay", 3, 0, PYTHON_BREW, "nexo-cognitive-decay.py", None),
|
|
150
|
+
("evolution", 3, 0, PYTHON_SYS, "nexo-evolution-run.py", 6), # Sunday = 6
|
|
151
|
+
("sleep", 4, 0, PYTHON_SYS, "nexo-sleep.py", None),
|
|
152
|
+
("self-audit", 7, 0, PYTHON_SYS, "nexo-daily-self-audit.py", None),
|
|
153
|
+
("github-monitor", 8, 0, PYTHON_BREW, "nexo-github-monitor.py", None),
|
|
154
|
+
("postmortem", 23, 30, PYTHON_BREW, "nexo-postmortem-consolidator.py", None),
|
|
138
155
|
]
|
|
139
156
|
|
|
140
157
|
ran = 0
|
|
141
158
|
skipped = 0
|
|
142
|
-
for name, hour, minute, script, weekday in tasks:
|
|
159
|
+
for name, hour, minute, python, script, weekday in tasks:
|
|
143
160
|
if should_run(name, hour, minute, state, weekday):
|
|
144
161
|
log(f" {name} — missed scheduled run, catching up...")
|
|
145
|
-
if run_task(name, script, state):
|
|
162
|
+
if run_task(name, python, script, state):
|
|
146
163
|
ran += 1
|
|
147
164
|
else:
|
|
148
165
|
skipped += 1
|
|
@@ -5,13 +5,11 @@ import json
|
|
|
5
5
|
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from datetime import datetime
|
|
8
|
-
import os
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
sys.path.insert(0, str(NEXO_HOME / "src"))
|
|
9
|
+
sys.path.insert(0, str(Path.home() / "claude" / "nexo-mcp"))
|
|
12
10
|
import cognitive
|
|
13
11
|
|
|
14
|
-
STATE_FILE =
|
|
12
|
+
STATE_FILE = Path.home() / "claude" / "operations" / ".catchup-state.json"
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
def update_catchup_state():
|