nexo-brain 5.3.19 → 5.3.21

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.
Files changed (211) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/nexo-brain.js +52 -10
  3. package/package.json +1 -1
  4. package/src/auto_update.py +11 -8
  5. package/src/dashboard/static/favicon 2.svg +32 -0
  6. package/src/dashboard/static/nexo-logo 2.png +0 -0
  7. package/src/dashboard/static/nexo-logo 2.svg +40 -0
  8. package/src/dashboard/static/style 2.css +2458 -0
  9. package/src/dashboard/templates/adaptive 2.html +118 -0
  10. package/src/dashboard/templates/artifacts 2.html +133 -0
  11. package/src/dashboard/templates/backups 2.html +136 -0
  12. package/src/dashboard/templates/base 2.html +417 -0
  13. package/src/dashboard/templates/calendar 2.html +591 -0
  14. package/src/dashboard/templates/chat 2.html +356 -0
  15. package/src/dashboard/templates/claims 2.html +259 -0
  16. package/src/dashboard/templates/cortex 2.html +321 -0
  17. package/src/dashboard/templates/credentials 2.html +128 -0
  18. package/src/dashboard/templates/crons 2.html +370 -0
  19. package/src/dashboard/templates/dashboard 2.html +494 -0
  20. package/src/dashboard/templates/dreams 2.html +252 -0
  21. package/src/dashboard/templates/email 2.html +160 -0
  22. package/src/dashboard/templates/evolution 2.html +189 -0
  23. package/src/dashboard/templates/feed 2.html +249 -0
  24. package/src/dashboard/templates/followup_health 2.html +170 -0
  25. package/src/dashboard/templates/graph 2.html +201 -0
  26. package/src/dashboard/templates/guard 2.html +259 -0
  27. package/src/dashboard/templates/inbox 2.html +251 -0
  28. package/src/dashboard/templates/memory 2.html +420 -0
  29. package/src/dashboard/templates/operations 2.html +608 -0
  30. package/src/dashboard/templates/plugins 2.html +185 -0
  31. package/src/dashboard/templates/protocol 2.html +199 -0
  32. package/src/dashboard/templates/rules 2.html +246 -0
  33. package/src/dashboard/templates/sentiment 2.html +247 -0
  34. package/src/dashboard/templates/sessions 2.html +218 -0
  35. package/src/dashboard/templates/skills 2.html +329 -0
  36. package/src/dashboard/templates/somatic 2.html +73 -0
  37. package/src/dashboard/templates/triggers 2.html +133 -0
  38. package/src/dashboard/templates/trust 2.html +360 -0
  39. package/src/db/__init__ 2.py +259 -0
  40. package/src/db/_core 2.py +437 -0
  41. package/src/db/_credentials 2.py +124 -0
  42. package/src/db/_episodic 2.py +762 -0
  43. package/src/db/_evolution 2.py +54 -0
  44. package/src/db/_fts 2.py +406 -0
  45. package/src/db/_goal_profiles 2.py +376 -0
  46. package/src/db/_hot_context 2.py +660 -0
  47. package/src/db/_outcomes 2.py +800 -0
  48. package/src/db/_personal_scripts 2.py +582 -0
  49. package/src/db/_sessions 2.py +330 -0
  50. package/src/db/_tasks 2.py +91 -0
  51. package/src/db/_watchers 2.py +173 -0
  52. package/src/doctor/formatters 2.py +52 -0
  53. package/src/doctor/models 2.py +69 -0
  54. package/src/doctor/planes 2.py +87 -0
  55. package/src/doctor/providers/__init__ 2.py +1 -0
  56. package/src/doctor/providers/deep 2.py +367 -0
  57. package/src/evolution_cycle 2.py +519 -0
  58. package/src/hooks/auto_capture 2.py +208 -0
  59. package/src/hooks/caffeinate-guard 2.sh +8 -0
  60. package/src/hooks/capture-session 2.sh +21 -0
  61. package/src/hooks/capture-tool-logs 2.sh +158 -0
  62. package/src/hooks/daily-briefing-check 2.sh +33 -0
  63. package/src/hooks/heartbeat-enforcement 2.py +90 -0
  64. package/src/hooks/heartbeat-posttool 2.sh +18 -0
  65. package/src/hooks/inbox-hook 2.sh +76 -0
  66. package/src/hooks/post-compact 2.sh +152 -0
  67. package/src/hooks/pre-compact 2.sh +169 -0
  68. package/src/hooks/protocol-guardrail 2.sh +10 -0
  69. package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
  70. package/src/hooks/session-stop 2.sh +52 -0
  71. package/src/kg_populate 2.py +292 -0
  72. package/src/maintenance 2.py +53 -0
  73. package/src/memory_backends 2.py +71 -0
  74. package/src/migrate_embeddings 2.py +124 -0
  75. package/src/nexo_sdk 2.py +103 -0
  76. package/src/observability 2.py +199 -0
  77. package/src/plugin_loader 2.py +217 -0
  78. package/src/plugins/__init__ 2.py +0 -0
  79. package/src/plugins/artifact_registry 2.py +450 -0
  80. package/src/plugins/backup 2.py +127 -0
  81. package/src/plugins/claims_tools 2.py +119 -0
  82. package/src/plugins/cognitive_memory 2.py +609 -0
  83. package/src/plugins/core_rules 2.py +252 -0
  84. package/src/plugins/cortex 2.py +1155 -0
  85. package/src/plugins/entities 2.py +67 -0
  86. package/src/plugins/episodic_memory 2.py +560 -0
  87. package/src/plugins/evolution 2.py +167 -0
  88. package/src/plugins/goal_engine 2.py +142 -0
  89. package/src/plugins/guard 2.py +862 -0
  90. package/src/plugins/impact 2.py +29 -0
  91. package/src/plugins/knowledge_graph_tools 2.py +137 -0
  92. package/src/plugins/media_memory_tools 2.py +98 -0
  93. package/src/plugins/memory_export 2.py +196 -0
  94. package/src/plugins/outcomes 2.py +130 -0
  95. package/src/plugins/personal_scripts 2.py +117 -0
  96. package/src/plugins/preferences 2.py +47 -0
  97. package/src/plugins/protocol 2.py +1449 -0
  98. package/src/plugins/simple_api 2.py +106 -0
  99. package/src/plugins/skills 2.py +341 -0
  100. package/src/plugins/state_watchers 2.py +79 -0
  101. package/src/plugins/update 2.py +986 -0
  102. package/src/plugins/user_state_tools 2.py +43 -0
  103. package/src/plugins/workflow 2.py +588 -0
  104. package/src/protocol_settings 2.py +59 -0
  105. package/src/public_contribution 2.py +466 -0
  106. package/src/public_evolution_queue 2.py +241 -0
  107. package/src/requirements 2.txt +14 -0
  108. package/src/retroactive_learnings 2.py +373 -0
  109. package/src/rules/__init__ 2.py +0 -0
  110. package/src/rules/core-rules 2.json +331 -0
  111. package/src/rules/migrate 2.py +207 -0
  112. package/src/runtime_power 2.py +874 -0
  113. package/src/script_registry 2.py +1559 -0
  114. package/src/scripts/check-context 2.py +272 -0
  115. package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
  116. package/src/scripts/deep-sleep/collect 2.py +928 -0
  117. package/src/scripts/deep-sleep/extract 2.py +330 -0
  118. package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
  119. package/src/scripts/deep-sleep/synthesize 2.py +312 -0
  120. package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
  121. package/src/scripts/nexo-agent-run 2.py +75 -0
  122. package/src/scripts/nexo-auto-update 2.py +6 -0
  123. package/src/scripts/nexo-backup 2.sh +25 -0
  124. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  125. package/src/scripts/nexo-catchup 2.py +300 -0
  126. package/src/scripts/nexo-cognitive-decay 2.py +257 -0
  127. package/src/scripts/nexo-cortex-cycle 2.py +293 -0
  128. package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
  129. package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
  130. package/src/scripts/nexo-dashboard 2.sh +29 -0
  131. package/src/scripts/nexo-deep-sleep 2.sh +86 -0
  132. package/src/scripts/nexo-evolution-run 2.py +1664 -0
  133. package/src/scripts/nexo-followup-hygiene 2.py +139 -0
  134. package/src/scripts/nexo-hook-record 2.py +42 -0
  135. package/src/scripts/nexo-immune 2.py +936 -0
  136. package/src/scripts/nexo-impact-scorer 2.py +117 -0
  137. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  138. package/src/scripts/nexo-install 2.py +6 -0
  139. package/src/scripts/nexo-learning-housekeep 2.py +401 -0
  140. package/src/scripts/nexo-learning-validator 2.py +266 -0
  141. package/src/scripts/nexo-migrate 2.py +260 -0
  142. package/src/scripts/nexo-outcome-checker 2.py +127 -0
  143. package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
  144. package/src/scripts/nexo-pre-commit 2.py +120 -0
  145. package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
  146. package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
  147. package/src/scripts/nexo-reflection 2.py +256 -0
  148. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  149. package/src/scripts/nexo-sleep 2.py +631 -0
  150. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  151. package/src/scripts/nexo-sync-clients 2.py +16 -0
  152. package/src/scripts/nexo-synthesis 2.py +475 -0
  153. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  154. package/src/scripts/nexo-update 2.sh +306 -0
  155. package/src/scripts/nexo-watchdog 2.sh +1207 -0
  156. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  157. package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
  158. package/src/server 2.py +1296 -0
  159. package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
  160. package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
  161. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
  162. package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
  163. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
  164. package/src/skills/run-release-final-audit/guide 2.md +16 -0
  165. package/src/skills/run-release-final-audit/script 2.py +259 -0
  166. package/src/skills/run-release-final-audit/skill 2.json +77 -0
  167. package/src/skills/run-runtime-doctor/guide 2.md +12 -0
  168. package/src/skills/run-runtime-doctor/script 2.py +21 -0
  169. package/src/skills/run-runtime-doctor/skill 2.json +25 -0
  170. package/src/skills_runtime 2.py +932 -0
  171. package/src/state_watchers_runtime 2.py +475 -0
  172. package/src/storage_router 2.py +32 -0
  173. package/src/system_catalog 2.py +786 -0
  174. package/src/tools_coordination 2.py +103 -0
  175. package/src/tools_credentials 2.py +68 -0
  176. package/src/tools_drive 2.py +487 -0
  177. package/src/tools_hot_context 2.py +163 -0
  178. package/src/tools_learnings 2.py +612 -0
  179. package/src/tools_menu 2.py +229 -0
  180. package/src/tools_reminders 2.py +88 -0
  181. package/src/tools_reminders_crud 2.py +363 -0
  182. package/src/tools_sessions 2.py +1054 -0
  183. package/src/tools_system_catalog 2.py +19 -0
  184. package/src/tools_task_history 2.py +57 -0
  185. package/src/tools_transcripts 2.py +98 -0
  186. package/src/transcript_utils 2.py +412 -0
  187. package/src/user_context 2.py +46 -0
  188. package/src/user_data_portability 2.py +328 -0
  189. package/src/user_state_model 2.py +170 -0
  190. package/templates/CLAUDE.md 2.template +108 -0
  191. package/templates/CODEX.AGENTS.md 2.template +66 -0
  192. package/templates/launchagents/README 2.md +132 -0
  193. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
  194. package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
  195. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
  196. package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
  197. package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
  198. package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
  199. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
  200. package/templates/launchagents/com.nexo.immune 2.plist +41 -0
  201. package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
  202. package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
  203. package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
  204. package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
  205. package/templates/nexo_helper 2.py +301 -0
  206. package/templates/openclaw 2.json +13 -0
  207. package/templates/plugin-template 2.py +40 -0
  208. package/templates/script-template 2.py +59 -0
  209. package/templates/script-template 2.sh +13 -0
  210. package/templates/skill-script-template 2.py +48 -0
  211. package/templates/skill-template 2.md +33 -0
@@ -0,0 +1,609 @@
1
+ """Cognitive Memory plugin — RAG retrieval over NEXO's Atkinson-Shiffrin memory stores."""
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
+
11
+ import cognitive
12
+
13
+
14
+ def handle_cognitive_retrieve(
15
+ query: str,
16
+ top_k: int = 10,
17
+ min_score: float = 0.5,
18
+ stores: str = "both",
19
+ source_type: str = "",
20
+ domain: str = "",
21
+ include_archived: bool = False,
22
+ use_hyde: bool | None = None,
23
+ spreading_depth: int | None = None,
24
+ hybrid_alpha: float = 0.6,
25
+ decompose: bool = True,
26
+ exclude_dreams: bool = True,
27
+ exclude_dormant: bool = True,
28
+ ) -> str:
29
+ """RAG query over cognitive memory (STM + LTM). Triggers rehearsal on retrieved memories.
30
+
31
+ Args:
32
+ query: Natural language query to search for
33
+ top_k: Maximum number of results to return (default 10)
34
+ min_score: Minimum cosine similarity score (default 0.5)
35
+ stores: Which store to search — "both", "stm", or "ltm" (default "both")
36
+ source_type: Filter by source type e.g. "change", "learning", "diary" (default: all)
37
+ domain: Filter by domain e.g. "project-a", "shopify" (default: all)
38
+ include_archived: If True, also search archived memories (default False)
39
+ use_hyde: If True/False, force HyDE on/off. If omitted, NEXO auto-enables it for conceptual queries.
40
+ spreading_depth: If >0, boost co-activated neighbors directly. If omitted, NEXO may auto-enable shallow spreading for multi-hop queries.
41
+ hybrid_alpha: Weight for vector vs BM25 fusion (default 0.6)
42
+ decompose: If True, decompose complex queries into sub-queries (default True)
43
+ exclude_dreams: If True, exclude dream insights from retrieval by default
44
+ exclude_dormant: If True, keep dormant LTM out of results unless explicitly requested
45
+ """
46
+ if not query or not query.strip():
47
+ return "ERROR: query is required."
48
+
49
+ results = cognitive.search(
50
+ query_text=query,
51
+ top_k=top_k,
52
+ min_score=min_score,
53
+ stores=stores,
54
+ exclude_dormant=exclude_dormant,
55
+ rehearse=True,
56
+ source_type_filter=source_type,
57
+ include_archived=include_archived,
58
+ use_hyde=use_hyde,
59
+ spreading_depth=spreading_depth,
60
+ hybrid_alpha=hybrid_alpha,
61
+ decompose=decompose,
62
+ exclude_dreams=exclude_dreams,
63
+ )
64
+
65
+ # Apply domain filter post-search (cognitive.search doesn't filter by domain natively)
66
+ if domain:
67
+ results = [r for r in results if r.get("domain", "") == domain]
68
+
69
+ formatted = cognitive.format_results(results)
70
+ mode_parts = [f"stores={stores}", f"min_score={min_score}"]
71
+ if use_hyde is True:
72
+ mode_parts.append("hyde=ON")
73
+ elif use_hyde is None:
74
+ mode_parts.append("hyde=AUTO")
75
+ if spreading_depth and spreading_depth > 0:
76
+ mode_parts.append(f"spreading={spreading_depth}")
77
+ elif spreading_depth is None:
78
+ mode_parts.append("spreading=AUTO")
79
+ mode_parts.append(f"hybrid_alpha={hybrid_alpha}")
80
+ mode_parts.append(f"decompose={'ON' if decompose else 'OFF'}")
81
+ mode_parts.append(f"dreams={'OFF' if exclude_dreams else 'ON'}")
82
+ mode_parts.append(f"dormant={'OFF' if exclude_dormant else 'ON'}")
83
+ if results:
84
+ top_score = float(results[0].get("score", 0.0) or 0.0)
85
+ confidence = "high" if top_score >= 0.82 else "medium" if top_score >= 0.66 else "low"
86
+ mode_parts.append(f"top_confidence={confidence}")
87
+ header = f"COGNITIVE RETRIEVE — query: '{query}' | {len(results)} results ({', '.join(mode_parts)})\n\n"
88
+ return header + formatted
89
+
90
+
91
+ def handle_cognitive_stats() -> str:
92
+ """Return cognitive memory system metrics: STM/LTM counts, strengths, retrieval stats, top domains."""
93
+ stats = cognitive.get_stats()
94
+
95
+ lines = [
96
+ "COGNITIVE MEMORY STATS",
97
+ f" STM active: {stats['stm_active']} (+ {stats.get('stm_promoted', 0)} promoted to LTM, {stats.get('stm_total', 0)} total)",
98
+ f" LTM active: {stats['ltm_active']}",
99
+ f" LTM dormant: {stats['ltm_dormant']}",
100
+ f" Avg STM strength: {stats['avg_stm_strength']:.3f}",
101
+ f" Avg LTM strength: {stats['avg_ltm_strength']:.3f}",
102
+ f" Avg STM stability: {stats.get('avg_stm_stability', 0.0):.3f}",
103
+ f" Avg LTM stability: {stats.get('avg_ltm_stability', 0.0):.3f}",
104
+ f" Avg STM difficulty: {stats.get('avg_stm_difficulty', 0.0):.3f}",
105
+ f" Avg LTM difficulty: {stats.get('avg_ltm_difficulty', 0.0):.3f}",
106
+ f" Total retrievals: {stats['total_retrievals']}",
107
+ f" Avg retrieval score: {stats['avg_retrieval_score']:.3f}",
108
+ ]
109
+
110
+ if stats["top_domains_stm"]:
111
+ lines.append(" Top STM domains:")
112
+ for domain, cnt in stats["top_domains_stm"]:
113
+ lines.append(f" {domain}: {cnt}")
114
+
115
+ if stats["top_domains_ltm"]:
116
+ lines.append(" Top LTM domains:")
117
+ for domain, cnt in stats["top_domains_ltm"]:
118
+ lines.append(f" {domain}: {cnt}")
119
+
120
+ if "quarantine" in stats:
121
+ q = stats["quarantine"]
122
+ lines.append(f" Quarantine pending: {q.get('pending', 0)}")
123
+ lines.append(f" Quarantine promoted: {q.get('promoted', 0)}")
124
+ lines.append(f" Quarantine rejected: {q.get('rejected', 0)}")
125
+ lines.append(f" Quarantine expired: {q.get('expired', 0)}")
126
+
127
+ if "prediction_error_gate" in stats:
128
+ g = stats["prediction_error_gate"]
129
+ lines.append(" PE Gate (session):")
130
+ lines.append(f" Accepted (novel): {g['accepted_novel']}")
131
+ lines.append(f" Accepted (refine): {g['accepted_refinement']}")
132
+ lines.append(f" Rejected (redundant): {g['rejected']}")
133
+ lines.append(f" Rejection rate: {g['rejection_rate_pct']}%")
134
+
135
+ return "\n".join(lines)
136
+
137
+
138
+ def handle_cognitive_inspect(memory_id: int, store: str = "ltm") -> str:
139
+ """Inspect a specific memory by ID without triggering rehearsal.
140
+
141
+ Args:
142
+ memory_id: Integer ID of the memory to inspect
143
+ store: Which store to read from — "stm" or "ltm" (default "ltm")
144
+ """
145
+ if store not in ("stm", "ltm"):
146
+ return "ERROR: store must be 'stm' or 'ltm'."
147
+
148
+ db = cognitive._get_db()
149
+ table = "stm_memories" if store == "stm" else "ltm_memories"
150
+
151
+ row = db.execute(f"SELECT * FROM {table} WHERE id = ?", (memory_id,)).fetchone()
152
+ if row is None:
153
+ return f"ERROR: Memory #{memory_id} not found in {store.upper()}."
154
+
155
+ content_preview = row["content"][:500]
156
+ if len(row["content"]) > 500:
157
+ content_preview += "..."
158
+
159
+ lines = [
160
+ f"COGNITIVE INSPECT — {store.upper()} #{memory_id}",
161
+ f" source_type: {row['source_type']}",
162
+ f" source_id: {row['source_id']}",
163
+ f" source_title: {row['source_title']}",
164
+ f" domain: {row['domain']}",
165
+ f" strength: {row['strength']:.4f}",
166
+ f" access_count: {row['access_count']}",
167
+ f" created_at: {row['created_at']}",
168
+ f" last_accessed: {row['last_accessed']}",
169
+ ]
170
+
171
+ # Lifecycle state
172
+ lifecycle = row["lifecycle_state"] or "active"
173
+ lines.append(f" lifecycle: {lifecycle}")
174
+ if row["snooze_until"]:
175
+ lines.append(f" snooze_until: {row['snooze_until']}")
176
+
177
+ if store == "ltm":
178
+ dormant_label = "YES" if row["is_dormant"] else "no"
179
+ lines.append(f" dormant: {dormant_label}")
180
+ if row["tags"]:
181
+ lines.append(f" tags: {row['tags']}")
182
+
183
+ if store == "stm":
184
+ promoted_label = "YES" if row["promoted_to_ltm"] else "no"
185
+ lines.append(f" promoted: {promoted_label}")
186
+
187
+ lines.append(f" content:\n {content_preview}")
188
+
189
+ return "\n".join(lines)
190
+
191
+
192
+ def handle_cognitive_metrics(days: int = 7) -> str:
193
+ """Cognitive memory performance metrics (spec section 9).
194
+
195
+ Returns retrieval relevance %, repeat error rate, score distribution,
196
+ and whether multilingual model switch is recommended.
197
+
198
+ Args:
199
+ days: Period to analyze in days (default 7)
200
+ """
201
+ metrics = cognitive.get_metrics(days=days)
202
+ repeats = cognitive.check_repeat_errors()
203
+
204
+ lines = [
205
+ f"COGNITIVE METRICS — last {days} days",
206
+ "",
207
+ "Retrieval Performance:",
208
+ f" Total retrievals: {metrics['total_retrievals']}",
209
+ f" Retrievals/day: {metrics['retrievals_per_day']}",
210
+ f" Relevance (>=0.6): {metrics['retrieval_relevance_pct']}% (target: >60%)",
211
+ f" Avg top score: {metrics['avg_top_score']}",
212
+ "",
213
+ "Score Distribution:",
214
+ f" >0.8 (excellent): {metrics['score_distribution']['above_80']}",
215
+ f" 0.7-0.8 (good): {metrics['score_distribution']['70_80']}",
216
+ f" 0.6-0.7 (ok): {metrics['score_distribution']['60_70']}",
217
+ f" 0.5-0.6 (weak): {metrics['score_distribution']['50_60']}",
218
+ f" <0.5 (irrelevant): {metrics['score_distribution']['below_50']}",
219
+ "",
220
+ "Repeat Error Rate:",
221
+ f" New learnings (7d): {repeats['new_count']}",
222
+ f" Duplicates found: {repeats['duplicate_count']}",
223
+ f" Repeat rate: {repeats['repeat_rate_pct']}% (target: <10%)",
224
+ ]
225
+
226
+ if metrics["needs_multilingual"]:
227
+ lines.append("")
228
+ lines.append("⚠ RECOMMENDATION: Switch to multilingual model (intfloat/multilingual-e5-small)")
229
+ lines.append(f" Reason: relevance {metrics['retrieval_relevance_pct']}% < 70% with {metrics['total_retrievals']}+ retrievals")
230
+
231
+ if repeats["duplicates"]:
232
+ lines.append("")
233
+ lines.append("Top duplicates:")
234
+ for d in repeats["duplicates"][:5]:
235
+ lines.append(f" [{d['score']}] STM#{d['new_stm_id']}: {d['new_content'][:60]}...")
236
+ lines.append(f" ≈ LTM#{d['ltm_id']}: {d['ltm_content'][:60]}...")
237
+
238
+ # Prediction Error Gate stats
239
+ gate = cognitive.get_gate_stats()
240
+ if gate["total_evaluated"] > 0:
241
+ lines.append("")
242
+ lines.append("Prediction Error Gate (session):")
243
+ lines.append(f" Novel accepted: {gate['accepted_novel']}")
244
+ lines.append(f" Refinements: {gate['accepted_refinement']}")
245
+ lines.append(f" Rejected redundant: {gate['rejected']}")
246
+ lines.append(f" Rejection rate: {gate['rejection_rate_pct']}%")
247
+
248
+ return "\n".join(lines)
249
+
250
+
251
+ def handle_cognitive_sentiment(text: str) -> str:
252
+ """Detect user's sentiment from his text. Returns mood, intensity, and guidance.
253
+
254
+ Call this with user's recent message to adapt NEXO's tone and behavior.
255
+ Also logs the sentiment for historical tracking.
256
+
257
+ Args:
258
+ text: user's recent message or instruction
259
+ """
260
+ result = cognitive.log_sentiment(text)
261
+ trust = cognitive.get_trust_score()
262
+
263
+ lines = [
264
+ f"SENTIMENT: {result['sentiment'].upper()} (intensity: {result['intensity']})",
265
+ f"Trust Score: {trust:.0f}/100",
266
+ ]
267
+ if result["signals"]:
268
+ lines.append(f"Signals: {', '.join(result['signals'])}")
269
+ if result["guidance"]:
270
+ lines.append(f"Guidance: {result['guidance']}")
271
+
272
+ return "\n".join(lines)
273
+
274
+
275
+ def handle_cognitive_trust(event: str = '', context: str = '', delta: float = None) -> str:
276
+ """View or adjust the trust score (alignment index 0-100).
277
+
278
+ Without arguments: shows current score and recent history.
279
+ With event: adjusts score based on event type.
280
+
281
+ Args:
282
+ event: Event type — explicit_thanks, delegation, paradigm_shift, sibling_detected,
283
+ proactive_action, correction, repeated_error, override, correction_fatigue,
284
+ forgot_followup. Or empty to just view.
285
+ context: Description of what happened
286
+ delta: Custom point value (overrides default for the event type)
287
+ """
288
+ if not event:
289
+ # View mode
290
+ trust = cognitive.get_trust_score()
291
+ history = cognitive.get_trust_history(days=7)
292
+
293
+ lines = [
294
+ f"TRUST SCORE: {trust:.0f}/100",
295
+ f"7-day change: {history['net_change']:+.0f} (from {history['period_start_score']:.0f})",
296
+ "",
297
+ ]
298
+
299
+ if history["sentiment_distribution"]:
300
+ lines.append("Sentiment (7d):")
301
+ for sent, data in history["sentiment_distribution"].items():
302
+ lines.append(f" {sent}: {data['count']}x (avg intensity {data['avg_intensity']})")
303
+ lines.append("")
304
+
305
+ if history["events"]:
306
+ lines.append("Recent events:")
307
+ for e in history["events"][-10:]:
308
+ lines.append(f" [{e['delta']:+.0f}] {e['event']}: {e['context'][:60]} ({e['at'][:16]})")
309
+
310
+ return "\n".join(lines)
311
+
312
+ # Adjust mode
313
+ result = cognitive.adjust_trust(event, context, delta)
314
+ if "error" in result:
315
+ valid = ", ".join(sorted(cognitive.TRUST_EVENTS.keys()))
316
+ return f"Unknown event '{event}'. Valid: {valid}"
317
+
318
+ return f"Trust: {result['old_score']:.0f} → {result['new_score']:.0f} ({result['delta']:+.0f}) [{event}]"
319
+
320
+
321
+ def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
322
+ """Detect cognitive dissonance: find established memories that conflict with a new instruction.
323
+
324
+ Use BEFORE applying a new preference or rule from user that might contradict
325
+ existing knowledge. If conflicts found, verbalize them and ask user to resolve.
326
+
327
+ Args:
328
+ instruction: The new instruction or preference to check against LTM
329
+ force: If True, skip discussion — execute instruction, auto-resolve all conflicts as
330
+ 'exception', and flag for review in the nocturnal process (23:30).
331
+ """
332
+ conflicts = cognitive.detect_dissonance(instruction)
333
+ if not conflicts:
334
+ return f"No dissonance detected. Instruction '{instruction[:80]}' is consistent with existing LTM."
335
+
336
+ if force:
337
+ # Auto-resolve all as exceptions, log for nocturnal review
338
+ for c in conflicts:
339
+ cognitive.resolve_dissonance(
340
+ c["memory_id"], "exception",
341
+ f"[FORCE] {instruction[:200]} — auto-exception, pending nocturnal review"
342
+ )
343
+ return (f"FORCE: {len(conflicts)} conflicts auto-resolved as exceptions. "
344
+ f"Instruction executed. Flagged for review at 23:30.")
345
+
346
+ lines = [
347
+ f"COGNITIVE DISSONANCE DETECTED — {len(conflicts)} conflicting memories:",
348
+ f"New instruction: \"{instruction[:200]}\"",
349
+ "",
350
+ ]
351
+ for c in conflicts:
352
+ lines.append(f" LTM #{c['memory_id']} [{c['source_type']}] (strength={c['strength']:.2f}, {c['access_count']} accesses)")
353
+ lines.append(f" Similarity: {c['similarity']}")
354
+ lines.append(f" Content: {c['content'][:200]}")
355
+ lines.append("")
356
+
357
+ lines.append("RESOLVE with nexo_cognitive_resolve, or use force=True to skip:")
358
+ lines.append(" - 'paradigm_shift': user changed his mind permanently.")
359
+ lines.append(" - 'exception': One-time override. Old memory stays.")
360
+ lines.append(" - 'override': Old memory was wrong.")
361
+
362
+ return "\n".join(lines)
363
+
364
+
365
+ def handle_cognitive_resolve(memory_id: int, resolution: str, context: str = '') -> str:
366
+ """Resolve a cognitive dissonance by applying user's decision.
367
+
368
+ Args:
369
+ memory_id: The LTM memory ID from the dissonance detection
370
+ resolution: 'paradigm_shift' (permanent change), 'exception' (one-time), or 'override' (old was wrong)
371
+ context: Optional context about why this resolution was chosen
372
+ """
373
+ return cognitive.resolve_dissonance(memory_id, resolution, context)
374
+
375
+
376
+ def handle_cognitive_pin(memory_id: int, store: str = "auto") -> str:
377
+ """Pin a memory so it NEVER decays and gets boosted in search results (+0.2 similarity).
378
+
379
+ Args:
380
+ memory_id: Integer ID of the memory to pin
381
+ store: Which store — "stm", "ltm", or "auto" (tries both, default "auto")
382
+ """
383
+ return cognitive.set_lifecycle(memory_id, "pinned", store)
384
+
385
+
386
+ def handle_cognitive_snooze(memory_id: int, until_date: str, store: str = "auto") -> str:
387
+ """Snooze a memory — hidden from searches until the given date, then auto-restores to active.
388
+
389
+ Args:
390
+ memory_id: Integer ID of the memory to snooze
391
+ until_date: Date to restore the memory (YYYY-MM-DD format)
392
+ store: Which store — "stm", "ltm", or "auto" (tries both, default "auto")
393
+ """
394
+ return cognitive.set_lifecycle(memory_id, "snoozed", store, snooze_until=until_date)
395
+
396
+
397
+ def handle_cognitive_archive(memory_id: int, store: str = "auto") -> str:
398
+ """Archive a memory — stored but excluded from normal searches. Can be restored later.
399
+
400
+ Args:
401
+ memory_id: Integer ID of the memory to archive
402
+ store: Which store — "stm", "ltm", or "auto" (tries both, default "auto")
403
+ """
404
+ return cognitive.set_lifecycle(memory_id, "archived", store)
405
+
406
+
407
+ def handle_cognitive_restore(memory_id: int, store: str = "auto") -> str:
408
+ """Restore a memory to active state (from pinned, snoozed, or archived).
409
+
410
+ Args:
411
+ memory_id: Integer ID of the memory to restore
412
+ store: Which store — "stm", "ltm", or "auto" (tries both, default "auto")
413
+ """
414
+ return cognitive.set_lifecycle(memory_id, "active", store)
415
+
416
+
417
+ def handle_cognitive_quarantine_list(status: str = "pending", limit: int = 20) -> str:
418
+ """List quarantine queue items. Shows memories awaiting promotion to STM.
419
+
420
+ Args:
421
+ status: Filter — 'pending', 'promoted', 'rejected', 'expired', or 'all' (default 'pending')
422
+ limit: Max items to return (default 20)
423
+ """
424
+ items = cognitive.quarantine_list(status=status, limit=limit)
425
+ stats = cognitive.quarantine_stats()
426
+
427
+ lines = [
428
+ f"QUARANTINE QUEUE — {stats['pending']} pending | {stats['promoted']} promoted | {stats['rejected']} rejected | {stats['expired']} expired",
429
+ f"Showing: {status} (limit {limit})",
430
+ "",
431
+ ]
432
+
433
+ if not items:
434
+ lines.append("No items found.")
435
+ else:
436
+ for item in items:
437
+ lines.append(f" #{item['id']} [{item['status']}] source={item['source']} type={item['source_type']} domain={item['domain'] or '-'}")
438
+ lines.append(f" confidence={item['confidence']:.1f} checks={item['promotion_checks']} created={item['created_at'][:16]}")
439
+ if item['promoted_at']:
440
+ lines.append(f" promoted_at={item['promoted_at'][:16]}")
441
+ lines.append(f" {item['content']}")
442
+ lines.append("")
443
+
444
+ return "\n".join(lines)
445
+
446
+
447
+ def handle_cognitive_quarantine_promote(quarantine_id: int) -> str:
448
+ """Manually promote a quarantine item to STM, bypassing the automatic promotion policy.
449
+
450
+ Args:
451
+ quarantine_id: ID of the quarantine entry to promote
452
+ """
453
+ return cognitive.quarantine_promote(quarantine_id)
454
+
455
+
456
+ def handle_cognitive_quarantine_reject(quarantine_id: int, reason: str = "") -> str:
457
+ """Manually reject a quarantine item.
458
+
459
+ Args:
460
+ quarantine_id: ID of the quarantine entry to reject
461
+ reason: Optional reason for rejection
462
+ """
463
+ return cognitive.quarantine_reject(quarantine_id, reason)
464
+
465
+
466
+ def handle_cognitive_quarantine_process() -> str:
467
+ """Run the quarantine promotion cycle. Evaluates all pending items against the promotion policy.
468
+
469
+ Promotion rules:
470
+ - source='user_direct' → already promoted at ingest
471
+ - source='inferred' + second occurrence found → promote
472
+ - source='agent_observation' + >24h old + no LTM contradiction → promote
473
+ - Contradicts LTM (cosine >0.8) → reject
474
+ - >7 days old → expire
475
+ """
476
+ result = cognitive.process_quarantine()
477
+ lines = [
478
+ "QUARANTINE PROCESSING COMPLETE",
479
+ f" Promoted: {result['promoted']}",
480
+ f" Rejected: {result['rejected']}",
481
+ f" Expired: {result['expired']}",
482
+ f" Still pending: {result['still_pending']}",
483
+ f" Total: {result['total_processed']}",
484
+ ]
485
+ return "\n".join(lines)
486
+
487
+
488
+ # ============================================================================
489
+ # Prospective Memory trigger handlers (Feature 3)
490
+ # ============================================================================
491
+
492
+ def handle_cognitive_trigger_create(pattern: str, action: str, context: str = "") -> str:
493
+ """Create a prospective memory trigger — fires when text matches pattern.
494
+
495
+ Args:
496
+ pattern: Keywords to match (case-insensitive, comma-separated for OR matching)
497
+ action: What to do / remind about when the trigger fires
498
+ context: Optional context about why this trigger was created
499
+ """
500
+ trigger_id = cognitive.create_trigger(pattern, action, context)
501
+ return f"Trigger #{trigger_id} created — armed. Pattern: '{pattern}' | Action: '{action}'"
502
+
503
+
504
+ def handle_cognitive_trigger_list(status: str = "armed") -> str:
505
+ """List prospective memory triggers.
506
+
507
+ Args:
508
+ status: Filter — 'armed' (active, waiting), 'fired' (already triggered), 'all'
509
+ """
510
+ triggers = cognitive.list_triggers(status)
511
+ if not triggers:
512
+ return f"No {status} triggers found."
513
+
514
+ lines = [f"PROSPECTIVE TRIGGERS ({status}) — {len(triggers)} total", ""]
515
+ for t in triggers:
516
+ status_icon = "+" if t["status"] == "armed" else "x"
517
+ lines.append(f" [{status_icon}] #{t['id']} pattern='{t['trigger_pattern']}'")
518
+ lines.append(f" action: {t['action']}")
519
+ if t.get("context"):
520
+ lines.append(f" context: {t['context']}")
521
+ lines.append(f" created: {t['created_at'][:16]}")
522
+ if t.get("fired_at"):
523
+ lines.append(f" fired: {t['fired_at'][:16]}")
524
+ lines.append("")
525
+
526
+ return "\n".join(lines)
527
+
528
+
529
+ def handle_cognitive_trigger_check(text: str, use_semantic: bool = False) -> str:
530
+ """Check text against all armed triggers and fire matching ones.
531
+
532
+ Args:
533
+ text: Text to check against triggers (e.g. user message, heartbeat context)
534
+ use_semantic: Also use embedding similarity (slower but catches conceptual matches)
535
+ """
536
+ fired = cognitive.check_triggers(text, use_semantic=use_semantic)
537
+ if not fired:
538
+ return "No triggers fired."
539
+
540
+ lines = [f"TRIGGERS FIRED: {len(fired)}", ""]
541
+ for t in fired:
542
+ lines.append(f" #{t['id']} [{t['match_type']}] pattern='{t['pattern']}'")
543
+ lines.append(f" ACTION: {t['action']}")
544
+ if t.get("context"):
545
+ lines.append(f" context: {t['context']}")
546
+ lines.append("")
547
+
548
+ return "\n".join(lines)
549
+
550
+
551
+ def handle_cognitive_trigger_preview(text: str, use_semantic: bool = False) -> str:
552
+ """Preview prospective trigger matches without firing them."""
553
+ matches = cognitive.preview_triggers(text, use_semantic=use_semantic)
554
+ if not matches:
555
+ return "No anticipatory warnings."
556
+
557
+ lines = [f"ANTICIPATORY WARNINGS: {len(matches)}", ""]
558
+ for match in matches:
559
+ lines.append(f" #{match['id']} [{match['match_type']}] pattern='{match['pattern']}'")
560
+ lines.append(f" ACTION: {match['action']}")
561
+ if match.get("context"):
562
+ lines.append(f" context: {match['context']}")
563
+ lines.append("")
564
+
565
+ return "\n".join(lines)
566
+
567
+
568
+ def handle_cognitive_trigger_delete(trigger_id: int) -> str:
569
+ """Delete a prospective memory trigger.
570
+
571
+ Args:
572
+ trigger_id: ID of the trigger to delete
573
+ """
574
+ return cognitive.delete_trigger(trigger_id)
575
+
576
+
577
+ def handle_cognitive_trigger_rearm(trigger_id: int) -> str:
578
+ """Re-arm a fired trigger so it can fire again.
579
+
580
+ Args:
581
+ trigger_id: ID of the trigger to re-arm
582
+ """
583
+ return cognitive.rearm_trigger(trigger_id)
584
+
585
+
586
+ TOOLS = [
587
+ (handle_cognitive_retrieve, "nexo_cognitive_retrieve", "RAG query over cognitive memory (STM+LTM). Triggers rehearsal on retrieved results."),
588
+ (handle_cognitive_stats, "nexo_cognitive_stats", "Cognitive memory system metrics: STM/LTM counts, strengths, retrieval stats, quarantine counts"),
589
+ (handle_cognitive_inspect, "nexo_cognitive_inspect", "Inspect a specific memory by ID (debug). Does NOT trigger rehearsal."),
590
+ (handle_cognitive_metrics, "nexo_cognitive_metrics", "Performance metrics: retrieval relevance %, repeat error rate, multilingual recommendation (spec section 9)"),
591
+ (handle_cognitive_dissonance, "nexo_cognitive_dissonance", "Detect conflicts between a new instruction and established LTM memories. force=True to skip discussion."),
592
+ (handle_cognitive_resolve, "nexo_cognitive_resolve", "Resolve a cognitive dissonance: paradigm_shift, exception, or override."),
593
+ (handle_cognitive_sentiment, "nexo_cognitive_sentiment", "Detect user's sentiment and get tone guidance. Also logs for tracking."),
594
+ (handle_cognitive_trust, "nexo_cognitive_trust", "View or adjust trust score (0-100). Without args: view. With event: adjust."),
595
+ (handle_cognitive_pin, "nexo_cognitive_pin", "Pin a memory — never decays, boosted +0.2 in search results."),
596
+ (handle_cognitive_snooze, "nexo_cognitive_snooze", "Snooze a memory — hidden from searches until a date, then auto-restores."),
597
+ (handle_cognitive_archive, "nexo_cognitive_archive", "Archive a memory — excluded from searches, can be restored."),
598
+ (handle_cognitive_restore, "nexo_cognitive_restore", "Restore a memory to active state (from pinned/snoozed/archived)."),
599
+ (handle_cognitive_quarantine_list, "nexo_cognitive_quarantine_list", "List quarantine queue items awaiting promotion to STM."),
600
+ (handle_cognitive_quarantine_promote, "nexo_cognitive_quarantine_promote", "Manually promote a quarantine item to STM."),
601
+ (handle_cognitive_quarantine_reject, "nexo_cognitive_quarantine_reject", "Manually reject a quarantine item."),
602
+ (handle_cognitive_quarantine_process, "nexo_cognitive_quarantine_process", "Run quarantine promotion cycle — evaluate pending items against policy."),
603
+ (handle_cognitive_trigger_create, "nexo_cognitive_trigger_create", "Create a prospective memory trigger — 'when X is mentioned, remind about Y'."),
604
+ (handle_cognitive_trigger_list, "nexo_cognitive_trigger_list", "List prospective triggers by status (armed/fired/all)."),
605
+ (handle_cognitive_trigger_preview, "nexo_cognitive_trigger_preview", "Preview anticipatory trigger matches without firing them."),
606
+ (handle_cognitive_trigger_check, "nexo_cognitive_trigger_check", "Check text against armed triggers. Returns fired triggers with actions."),
607
+ (handle_cognitive_trigger_delete, "nexo_cognitive_trigger_delete", "Delete a prospective trigger by ID."),
608
+ (handle_cognitive_trigger_rearm, "nexo_cognitive_trigger_rearm", "Re-arm a fired trigger so it can fire again."),
609
+ ]