nexo-brain 5.3.26 → 5.3.28

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 (212) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/src/hook_guardrails.py +44 -0
  4. package/src/server.py +3 -0
  5. package/src/tools_sessions.py +6 -1
  6. package/src/dashboard/static/favicon 2.svg +0 -32
  7. package/src/dashboard/static/nexo-logo 2.png +0 -0
  8. package/src/dashboard/static/nexo-logo 2.svg +0 -40
  9. package/src/dashboard/static/style 2.css +0 -2458
  10. package/src/dashboard/templates/adaptive 2.html +0 -118
  11. package/src/dashboard/templates/artifacts 2.html +0 -133
  12. package/src/dashboard/templates/backups 2.html +0 -136
  13. package/src/dashboard/templates/base 2.html +0 -417
  14. package/src/dashboard/templates/calendar 2.html +0 -591
  15. package/src/dashboard/templates/chat 2.html +0 -356
  16. package/src/dashboard/templates/claims 2.html +0 -259
  17. package/src/dashboard/templates/cortex 2.html +0 -321
  18. package/src/dashboard/templates/credentials 2.html +0 -128
  19. package/src/dashboard/templates/crons 2.html +0 -370
  20. package/src/dashboard/templates/dashboard 2.html +0 -494
  21. package/src/dashboard/templates/dreams 2.html +0 -252
  22. package/src/dashboard/templates/email 2.html +0 -160
  23. package/src/dashboard/templates/evolution 2.html +0 -189
  24. package/src/dashboard/templates/feed 2.html +0 -249
  25. package/src/dashboard/templates/followup_health 2.html +0 -170
  26. package/src/dashboard/templates/graph 2.html +0 -201
  27. package/src/dashboard/templates/guard 2.html +0 -259
  28. package/src/dashboard/templates/inbox 2.html +0 -251
  29. package/src/dashboard/templates/memory 2.html +0 -420
  30. package/src/dashboard/templates/operations 2.html +0 -608
  31. package/src/dashboard/templates/plugins 2.html +0 -185
  32. package/src/dashboard/templates/protocol 2.html +0 -199
  33. package/src/dashboard/templates/rules 2.html +0 -246
  34. package/src/dashboard/templates/sentiment 2.html +0 -247
  35. package/src/dashboard/templates/sessions 2.html +0 -218
  36. package/src/dashboard/templates/skills 2.html +0 -329
  37. package/src/dashboard/templates/somatic 2.html +0 -73
  38. package/src/dashboard/templates/triggers 2.html +0 -133
  39. package/src/dashboard/templates/trust 2.html +0 -360
  40. package/src/db/__init__ 2.py +0 -259
  41. package/src/db/_core 2.py +0 -437
  42. package/src/db/_credentials 2.py +0 -124
  43. package/src/db/_episodic 2.py +0 -762
  44. package/src/db/_evolution 2.py +0 -54
  45. package/src/db/_fts 2.py +0 -406
  46. package/src/db/_goal_profiles 2.py +0 -376
  47. package/src/db/_hot_context 2.py +0 -660
  48. package/src/db/_outcomes 2.py +0 -800
  49. package/src/db/_personal_scripts 2.py +0 -582
  50. package/src/db/_sessions 2.py +0 -330
  51. package/src/db/_tasks 2.py +0 -91
  52. package/src/db/_watchers 2.py +0 -173
  53. package/src/doctor/formatters 2.py +0 -52
  54. package/src/doctor/models 2.py +0 -69
  55. package/src/doctor/planes 2.py +0 -87
  56. package/src/doctor/providers/__init__ 2.py +0 -1
  57. package/src/doctor/providers/deep 2.py +0 -367
  58. package/src/evolution_cycle 2.py +0 -519
  59. package/src/hooks/auto_capture 2.py +0 -208
  60. package/src/hooks/caffeinate-guard 2.sh +0 -8
  61. package/src/hooks/capture-session 2.sh +0 -21
  62. package/src/hooks/capture-tool-logs 2.sh +0 -158
  63. package/src/hooks/daily-briefing-check 2.sh +0 -33
  64. package/src/hooks/heartbeat-enforcement 2.py +0 -90
  65. package/src/hooks/heartbeat-posttool 2.sh +0 -18
  66. package/src/hooks/inbox-hook 2.sh +0 -76
  67. package/src/hooks/post-compact 2.sh +0 -152
  68. package/src/hooks/pre-compact 2.sh +0 -169
  69. package/src/hooks/protocol-guardrail 2.sh +0 -10
  70. package/src/hooks/protocol-pretool-guardrail 2.sh +0 -9
  71. package/src/hooks/session-stop 2.sh +0 -52
  72. package/src/kg_populate 2.py +0 -292
  73. package/src/maintenance 2.py +0 -53
  74. package/src/memory_backends 2.py +0 -71
  75. package/src/migrate_embeddings 2.py +0 -124
  76. package/src/nexo_sdk 2.py +0 -103
  77. package/src/observability 2.py +0 -199
  78. package/src/plugin_loader 2.py +0 -217
  79. package/src/plugins/__init__ 2.py +0 -0
  80. package/src/plugins/artifact_registry 2.py +0 -450
  81. package/src/plugins/backup 2.py +0 -127
  82. package/src/plugins/claims_tools 2.py +0 -119
  83. package/src/plugins/cognitive_memory 2.py +0 -609
  84. package/src/plugins/core_rules 2.py +0 -252
  85. package/src/plugins/cortex 2.py +0 -1155
  86. package/src/plugins/entities 2.py +0 -67
  87. package/src/plugins/episodic_memory 2.py +0 -560
  88. package/src/plugins/evolution 2.py +0 -167
  89. package/src/plugins/goal_engine 2.py +0 -142
  90. package/src/plugins/guard 2.py +0 -862
  91. package/src/plugins/impact 2.py +0 -29
  92. package/src/plugins/knowledge_graph_tools 2.py +0 -137
  93. package/src/plugins/media_memory_tools 2.py +0 -98
  94. package/src/plugins/memory_export 2.py +0 -196
  95. package/src/plugins/outcomes 2.py +0 -130
  96. package/src/plugins/personal_scripts 2.py +0 -117
  97. package/src/plugins/preferences 2.py +0 -47
  98. package/src/plugins/protocol 2.py +0 -1449
  99. package/src/plugins/simple_api 2.py +0 -106
  100. package/src/plugins/skills 2.py +0 -341
  101. package/src/plugins/state_watchers 2.py +0 -79
  102. package/src/plugins/update 2.py +0 -986
  103. package/src/plugins/user_state_tools 2.py +0 -43
  104. package/src/plugins/workflow 2.py +0 -588
  105. package/src/protocol_settings 2.py +0 -59
  106. package/src/public_contribution 2.py +0 -466
  107. package/src/public_evolution_queue 2.py +0 -241
  108. package/src/requirements 2.txt +0 -14
  109. package/src/retroactive_learnings 2.py +0 -373
  110. package/src/rules/__init__ 2.py +0 -0
  111. package/src/rules/core-rules 2.json +0 -331
  112. package/src/rules/migrate 2.py +0 -207
  113. package/src/runtime_power 2.py +0 -874
  114. package/src/script_registry 2.py +0 -1559
  115. package/src/scripts/check-context 2.py +0 -272
  116. package/src/scripts/deep-sleep/apply_findings 2.py +0 -2327
  117. package/src/scripts/deep-sleep/collect 2.py +0 -928
  118. package/src/scripts/deep-sleep/extract 2.py +0 -330
  119. package/src/scripts/deep-sleep/extract-prompt 2.md +0 -285
  120. package/src/scripts/deep-sleep/synthesize 2.py +0 -312
  121. package/src/scripts/deep-sleep/synthesize-prompt 2.md +0 -336
  122. package/src/scripts/nexo-agent-run 2.py +0 -75
  123. package/src/scripts/nexo-auto-update 2.py +0 -6
  124. package/src/scripts/nexo-backup 2.sh +0 -25
  125. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  126. package/src/scripts/nexo-catchup 2.py +0 -300
  127. package/src/scripts/nexo-cognitive-decay 2.py +0 -257
  128. package/src/scripts/nexo-cortex-cycle 2.py +0 -293
  129. package/src/scripts/nexo-cron-wrapper 2.sh +0 -53
  130. package/src/scripts/nexo-daily-self-audit 2.py +0 -2161
  131. package/src/scripts/nexo-dashboard 2.sh +0 -29
  132. package/src/scripts/nexo-deep-sleep 2.sh +0 -86
  133. package/src/scripts/nexo-evolution-run 2.py +0 -1664
  134. package/src/scripts/nexo-followup-hygiene 2.py +0 -139
  135. package/src/scripts/nexo-hook-record 2.py +0 -42
  136. package/src/scripts/nexo-immune 2.py +0 -936
  137. package/src/scripts/nexo-impact-scorer 2.py +0 -117
  138. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  139. package/src/scripts/nexo-install 2.py +0 -6
  140. package/src/scripts/nexo-learning-housekeep 2.py +0 -401
  141. package/src/scripts/nexo-learning-validator 2.py +0 -266
  142. package/src/scripts/nexo-migrate 2.py +0 -260
  143. package/src/scripts/nexo-outcome-checker 2.py +0 -127
  144. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -456
  145. package/src/scripts/nexo-pre-commit 2.py +0 -120
  146. package/src/scripts/nexo-prevent-sleep 2.sh +0 -35
  147. package/src/scripts/nexo-proactive-dashboard 2.py +0 -354
  148. package/src/scripts/nexo-reflection 2.py +0 -256
  149. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  150. package/src/scripts/nexo-sleep 2.py +0 -631
  151. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  152. package/src/scripts/nexo-sync-clients 2.py +0 -16
  153. package/src/scripts/nexo-synthesis 2.py +0 -475
  154. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  155. package/src/scripts/nexo-update 2.sh +0 -306
  156. package/src/scripts/nexo-watchdog 2.sh +0 -1207
  157. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  158. package/src/scripts/rehydrate_learnings_from_archive 2.py +0 -245
  159. package/src/server 2.py +0 -1296
  160. package/src/skills/run-nexo-audit-phase/guide 2.md +0 -43
  161. package/src/skills/run-nexo-audit-phase/skill 2.json +0 -59
  162. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +0 -17
  163. package/src/skills/run-nexo-core-fix-cycle/script 2.py +0 -276
  164. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +0 -58
  165. package/src/skills/run-release-final-audit/guide 2.md +0 -16
  166. package/src/skills/run-release-final-audit/script 2.py +0 -259
  167. package/src/skills/run-release-final-audit/skill 2.json +0 -77
  168. package/src/skills/run-runtime-doctor/guide 2.md +0 -12
  169. package/src/skills/run-runtime-doctor/script 2.py +0 -21
  170. package/src/skills/run-runtime-doctor/skill 2.json +0 -25
  171. package/src/skills_runtime 2.py +0 -932
  172. package/src/state_watchers_runtime 2.py +0 -475
  173. package/src/storage_router 2.py +0 -32
  174. package/src/system_catalog 2.py +0 -786
  175. package/src/tools_coordination 2.py +0 -103
  176. package/src/tools_credentials 2.py +0 -68
  177. package/src/tools_drive 2.py +0 -487
  178. package/src/tools_hot_context 2.py +0 -163
  179. package/src/tools_learnings 2.py +0 -612
  180. package/src/tools_menu 2.py +0 -229
  181. package/src/tools_reminders 2.py +0 -88
  182. package/src/tools_reminders_crud 2.py +0 -363
  183. package/src/tools_sessions 2.py +0 -1054
  184. package/src/tools_system_catalog 2.py +0 -19
  185. package/src/tools_task_history 2.py +0 -57
  186. package/src/tools_transcripts 2.py +0 -98
  187. package/src/transcript_utils 2.py +0 -412
  188. package/src/user_context 2.py +0 -46
  189. package/src/user_data_portability 2.py +0 -328
  190. package/src/user_state_model 2.py +0 -170
  191. package/templates/CLAUDE.md 2.template +0 -108
  192. package/templates/CODEX.AGENTS.md 2.template +0 -66
  193. package/templates/launchagents/README 2.md +0 -132
  194. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +0 -39
  195. package/templates/launchagents/com.nexo.catchup 2.plist +0 -39
  196. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +0 -40
  197. package/templates/launchagents/com.nexo.dashboard 2.plist +0 -43
  198. package/templates/launchagents/com.nexo.deep-sleep 2.plist +0 -43
  199. package/templates/launchagents/com.nexo.evolution 2.plist +0 -44
  200. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +0 -45
  201. package/templates/launchagents/com.nexo.immune 2.plist +0 -41
  202. package/templates/launchagents/com.nexo.postmortem 2.plist +0 -45
  203. package/templates/launchagents/com.nexo.self-audit 2.plist +0 -47
  204. package/templates/launchagents/com.nexo.synthesis 2.plist +0 -45
  205. package/templates/launchagents/com.nexo.watchdog 2.plist +0 -37
  206. package/templates/nexo_helper 2.py +0 -301
  207. package/templates/openclaw 2.json +0 -13
  208. package/templates/plugin-template 2.py +0 -40
  209. package/templates/script-template 2.py +0 -59
  210. package/templates/script-template 2.sh +0 -13
  211. package/templates/skill-script-template 2.py +0 -48
  212. package/templates/skill-template 2.md +0 -33
@@ -1,612 +0,0 @@
1
- """Learnings CRUD tools: add, search, update, delete, list."""
2
-
3
- import re
4
- from datetime import datetime
5
-
6
- from db import (create_learning, update_learning, delete_learning, search_learnings,
7
- list_learnings, find_similar_learnings, get_db, now_epoch, supersede_learning)
8
-
9
- NEGATION_PATTERNS = (
10
- "do not", "don't", "never", "avoid", "skip", "without", "forbid", "forbidden",
11
- "disable", "disabled", "remove", "ban", "bypass",
12
- )
13
- CONTRADICTION_PAIRS = (
14
- ("enable", "disable"),
15
- ("use", "avoid"),
16
- ("add", "remove"),
17
- ("allow", "forbid"),
18
- ("always", "never"),
19
- ("before", "after"),
20
- ("require", "skip"),
21
- ("validate", "skip"),
22
- ("validate", "bypass"),
23
- ("include", "exclude"),
24
- )
25
-
26
-
27
- def _split_applies_to(applies_to: str) -> list[str]:
28
- return [item.strip() for item in str(applies_to or "").split(",") if item.strip()]
29
-
30
-
31
- def _normalize_applies_token(value: str) -> str:
32
- return str(value or "").replace("\\", "/").rstrip("/").lower()
33
-
34
-
35
- def _applies_overlap(left: str, right: str) -> bool:
36
- left_tokens = {_normalize_applies_token(item) for item in _split_applies_to(left)}
37
- right_tokens = {_normalize_applies_token(item) for item in _split_applies_to(right)}
38
- left_tokens.discard("")
39
- right_tokens.discard("")
40
- if not left_tokens or not right_tokens:
41
- return False
42
- if left_tokens & right_tokens:
43
- return True
44
- for left_token in left_tokens:
45
- for right_token in right_tokens:
46
- if "/" in left_token or "/" in right_token:
47
- if left_token.startswith(f"{right_token}/") or right_token.startswith(f"{left_token}/"):
48
- return True
49
- if left_token.endswith(f"/{right_token}") or right_token.endswith(f"/{left_token}"):
50
- return True
51
- return False
52
-
53
-
54
- def _normalize_text(text: str) -> str:
55
- return re.sub(r"\s+", " ", str(text or "").strip().lower())
56
-
57
-
58
- def _tokenize(text: str) -> list[str]:
59
- return re.findall(r"[a-z0-9_-]+", _normalize_text(text))
60
-
61
-
62
- def _contains_negation(text: str) -> bool:
63
- lowered = _normalize_text(text)
64
- return any(token in lowered for token in NEGATION_PATTERNS)
65
-
66
-
67
- def _negated_action_verbs(text: str) -> set[str]:
68
- lowered = _normalize_text(text)
69
- matches = set()
70
- for pattern in (
71
- r"(?:never|avoid|skip|disable|remove|forbid|bypass)\s+([a-z0-9_-]+)",
72
- r"(?:do not|don't)\s+([a-z0-9_-]+)",
73
- ):
74
- matches.update(re.findall(pattern, lowered))
75
- return {match for match in matches if len(match) > 2}
76
-
77
-
78
- def _looks_contradictory(existing_text: str, new_text: str) -> bool:
79
- existing_norm = _normalize_text(existing_text)
80
- new_norm = _normalize_text(new_text)
81
- if not existing_norm or not new_norm:
82
- return False
83
- existing_tokens = set(_tokenize(existing_norm))
84
- new_tokens = set(_tokenize(new_norm))
85
- if not (existing_tokens & new_tokens):
86
- return False
87
- existing_negated_verbs = _negated_action_verbs(existing_norm)
88
- new_negated_verbs = _negated_action_verbs(new_norm)
89
- if existing_negated_verbs & new_tokens and not existing_negated_verbs & new_negated_verbs:
90
- return True
91
- if new_negated_verbs & existing_tokens and not existing_negated_verbs & new_negated_verbs:
92
- return True
93
- if _contains_negation(existing_norm) != _contains_negation(new_norm):
94
- return True
95
- for positive, negative in CONTRADICTION_PAIRS:
96
- existing_has_pair = positive in existing_norm or negative in existing_norm
97
- new_has_pair = positive in new_norm or negative in new_norm
98
- if existing_has_pair and new_has_pair:
99
- if (positive in existing_norm and negative in new_norm) or (negative in existing_norm and positive in new_norm):
100
- return True
101
- return False
102
-
103
-
104
- def _find_conflicting_active_learning(conn, *, category: str, title: str, content: str,
105
- applies_to: str, exclude_id: int | None = None) -> dict | None:
106
- if not applies_to:
107
- return None
108
- params = [category]
109
- sql = (
110
- "SELECT id, title, content, applies_to FROM learnings "
111
- "WHERE category = ? AND status = 'active' AND COALESCE(applies_to, '') != ''"
112
- )
113
- if exclude_id is not None:
114
- sql += " AND id != ?"
115
- params.append(exclude_id)
116
- rows = conn.execute(sql, tuple(params)).fetchall()
117
- incoming_text = f"{title} {content}"
118
- for row in rows:
119
- if not _applies_overlap(row["applies_to"], applies_to):
120
- continue
121
- if _looks_contradictory(f"{row['title']} {row['content']}", incoming_text):
122
- return dict(row)
123
- return None
124
-
125
-
126
- def find_conflicting_active_learning(*, category: str, title: str, content: str,
127
- applies_to: str, exclude_id: int | None = None) -> dict | None:
128
- """Public wrapper for canonical-rule enforcement on file-scoped learnings."""
129
- conn = get_db()
130
- return _find_conflicting_active_learning(
131
- conn,
132
- category=category.lower().strip(),
133
- title=title,
134
- content=content,
135
- applies_to=applies_to,
136
- exclude_id=exclude_id,
137
- )
138
-
139
-
140
- def _priority_score(priority: str) -> float:
141
- return {
142
- "critical": 1.0,
143
- "high": 0.85,
144
- "medium": 0.65,
145
- "low": 0.45,
146
- }.get(str(priority or "medium").strip().lower(), 0.65)
147
-
148
-
149
- def _parse_timestamp(value) -> float:
150
- if isinstance(value, (int, float)):
151
- return float(value)
152
- text = str(value or "").strip()
153
- if not text:
154
- return 0.0
155
- try:
156
- return float(text)
157
- except Exception:
158
- pass
159
- for fmt in (None, "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
160
- try:
161
- if fmt is None:
162
- return datetime.fromisoformat(text.replace("Z", "+00:00")).timestamp()
163
- return datetime.strptime(text, fmt).timestamp()
164
- except Exception:
165
- continue
166
- return 0.0
167
-
168
-
169
- def _recency_score(row: dict) -> float:
170
- reference = _parse_timestamp(row.get("last_guard_hit_at")) or _parse_timestamp(row.get("updated_at")) or _parse_timestamp(row.get("created_at"))
171
- if not reference:
172
- return 0.35
173
- age_days = max(0.0, (now_epoch() - reference) / 86400.0)
174
- if age_days <= 7:
175
- return 1.0
176
- if age_days <= 30:
177
- return 0.8
178
- if age_days <= 90:
179
- return 0.6
180
- if age_days <= 180:
181
- return 0.4
182
- return 0.25
183
-
184
-
185
- def _usefulness_score(row: dict) -> float:
186
- hits = int(row.get("guard_hits") or 0)
187
- if hits >= 5:
188
- return 1.0
189
- if hits >= 3:
190
- return 0.85
191
- if hits >= 1:
192
- return 0.65
193
- return 0.35 if str(row.get("status") or "active") == "active" else 0.15
194
-
195
-
196
- def _source_richness_score(row: dict) -> float:
197
- parts = 0.0
198
- if str(row.get("reasoning") or "").strip():
199
- parts += 0.25
200
- if str(row.get("prevention") or "").strip():
201
- parts += 0.25
202
- if str(row.get("applies_to") or "").strip():
203
- parts += 0.2
204
- if row.get("review_due_at"):
205
- parts += 0.15
206
- if len(str(row.get("content") or "").strip()) >= 80:
207
- parts += 0.15
208
- return min(1.0, parts)
209
-
210
-
211
- def _contradiction_pressure_score(conn, row: dict) -> float:
212
- if str(row.get("status") or "active") != "active":
213
- return 0.7
214
- conflicting = _find_conflicting_active_learning(
215
- conn,
216
- category=str(row.get("category") or ""),
217
- title=str(row.get("title") or ""),
218
- content=str(row.get("content") or ""),
219
- applies_to=str(row.get("applies_to") or ""),
220
- exclude_id=int(row.get("id") or 0),
221
- )
222
- return 0.0 if conflicting else 1.0
223
-
224
-
225
- def score_learning_quality(row: dict, conn=None) -> dict:
226
- """Compute a 0-100 quality score for a learning using usefulness and conflict pressure."""
227
- conn = conn or get_db()
228
- confidence = min(1.0, (_priority_score(row.get("priority")) * 0.45) + (float(row.get("weight") or 0.5) * 0.55))
229
- usefulness = _usefulness_score(row)
230
- recency = _recency_score(row)
231
- contradiction = _contradiction_pressure_score(conn, row)
232
- source_richness = _source_richness_score(row)
233
- status = str(row.get("status") or "active")
234
- status_multiplier = 1.0 if status == "active" else 0.65 if status in {"pending_review", "review"} else 0.45
235
- overall = (
236
- confidence * 0.28
237
- + usefulness * 0.24
238
- + recency * 0.18
239
- + contradiction * 0.18
240
- + source_richness * 0.12
241
- ) * status_multiplier
242
- score = max(0, min(100, round(overall * 100)))
243
- if score >= 80:
244
- label = "strong"
245
- elif score >= 60:
246
- label = "usable"
247
- elif score >= 40:
248
- label = "weak"
249
- else:
250
- label = "fragile"
251
- return {
252
- "score": score,
253
- "label": label,
254
- "confidence": round(confidence * 100),
255
- "usefulness": round(usefulness * 100),
256
- "recency_relevance": round(recency * 100),
257
- "contradiction_pressure": round((1.0 - contradiction) * 100),
258
- "source_richness": round(source_richness * 100),
259
- }
260
-
261
-
262
- def handle_learning_add(category: str, title: str, content: str, reasoning: str = '',
263
- prevention: str = '', applies_to: str = '', review_days: int = 30,
264
- priority: str = 'medium', supersedes_id: int = 0) -> str:
265
- """Add a new learning entry to the specified category.
266
-
267
- Args:
268
- category: Free-form category name (e.g., 'backend', 'frontend', 'devops', 'infrastructure', 'security', 'nexo-ops'). Use consistent names — learnings are grouped and searched by category.
269
- title: Short title for the learning
270
- content: Full description of what was learned
271
- reasoning: WHY this matters — what led to discovering this, what was the context
272
- prevention: Concrete rule/check that prevents repeating this mistake
273
- applies_to: Files, systems, or areas this learning applies to
274
- review_days: Days until this learning should be reviewed again
275
- priority: critical, high, medium, low (default: medium)
276
- """
277
- if priority not in ('critical', 'high', 'medium', 'low'):
278
- priority = 'medium'
279
- category = category.lower().strip()
280
- if not category:
281
- return "ERROR: Category cannot be empty."
282
- # Dedup guard: block exact title duplicates in same category
283
- conn = get_db()
284
- existing = conn.execute(
285
- "SELECT id, title FROM learnings WHERE LOWER(title) = LOWER(?) AND category = ? AND status = 'active'",
286
- (title.strip(), category)
287
- ).fetchone()
288
- if existing:
289
- return f"Learning #{existing['id']} already exists with same title in {category}: {existing['title']}. Use nexo_learning_update to modify it."
290
- conflicting = _find_conflicting_active_learning(
291
- conn,
292
- category=category,
293
- title=title,
294
- content=content,
295
- applies_to=applies_to,
296
- )
297
- if conflicting and int(supersedes_id or 0) != int(conflicting["id"]):
298
- return (
299
- f"ERROR: Contradictory active learning #{conflicting['id']} already exists for applies_to="
300
- f"{conflicting.get('applies_to', '')}: {conflicting['title']}. "
301
- f"Supersede or update the existing canonical rule instead of creating two active file rules."
302
- )
303
- result = create_learning(
304
- category, title, content, reasoning=reasoning, supersedes_id=(int(supersedes_id) if supersedes_id else None)
305
- )
306
- if "error" in result:
307
- return f"ERROR: {result['error']}"
308
- if prevention or applies_to or review_days > 0 or priority != 'medium':
309
- initial_weight = {'critical': 0.9, 'high': 0.7, 'medium': 0.5, 'low': 0.3}[priority]
310
- updated_at = now_epoch()
311
- review_due_at = now_epoch() + (max(1, int(review_days)) * 86400)
312
- conn = get_db()
313
- conn.execute(
314
- "UPDATE learnings SET prevention = ?, applies_to = ?, status = COALESCE(status, 'active'), "
315
- "review_due_at = ?, updated_at = ?, priority = ?, weight = ? WHERE id = ?",
316
- (prevention, applies_to, review_due_at, updated_at,
317
- priority, initial_weight, result["id"])
318
- )
319
- conn.commit()
320
- result = dict(result)
321
- result.update({
322
- "prevention": prevention,
323
- "applies_to": applies_to,
324
- "status": result.get("status") or "active",
325
- "review_due_at": review_due_at,
326
- "updated_at": updated_at,
327
- "priority": priority,
328
- "weight": initial_weight,
329
- })
330
-
331
- # Cognitive ingest — embed learning for semantic search
332
- new_id = result["id"]
333
- try:
334
- import cognitive
335
- cognitive.ingest(f"{title}: {content}", "learning", f"L{new_id}", title, category)
336
- except Exception:
337
- pass
338
-
339
- # Similarity check — detect repeated errors
340
- matches = find_similar_learnings(new_id, title, content, category)
341
- repetition_msg = ""
342
- if matches:
343
- conn = get_db()
344
- for original_id, similarity in matches:
345
- conn.execute(
346
- "INSERT INTO error_repetitions (new_learning_id, original_learning_id, similarity, area) VALUES (?,?,?,?)",
347
- (new_id, original_id, similarity, category)
348
- )
349
- conn.commit()
350
- repetition_msg = f"\n⚠️ REPETITION WARNING: Similar to {len(matches)} existing learning(s): " + \
351
- ", ".join(f"#{m[0]} ({m[1]:.0%})" for m in matches[:3])
352
-
353
- # Somatic event logging (append-only in nexo.db, projected to cognitive.db nightly)
354
- try:
355
- if applies_to:
356
- for file_path in [f.strip() for f in applies_to.split(",") if f.strip()]:
357
- get_db().execute(
358
- "INSERT INTO somatic_events (target, target_type, event_type, delta, source) VALUES (?, ?, ?, ?, ?)",
359
- (file_path, "file", "learning_add", 0.15, f"learning:{new_id}")
360
- )
361
- # Area + extra file pain ONLY for repeated errors
362
- if matches:
363
- get_db().execute(
364
- "INSERT INTO somatic_events (target, target_type, event_type, delta, source) VALUES (?, ?, ?, ?, ?)",
365
- (category, "area", "error_repetition", 0.15, f"learning:{new_id}")
366
- )
367
- if applies_to:
368
- for file_path in [f.strip() for f in applies_to.split(",") if f.strip()]:
369
- get_db().execute(
370
- "INSERT INTO somatic_events (target, target_type, event_type, delta, source) VALUES (?, ?, ?, ?, ?)",
371
- (file_path, "file", "error_repetition", 0.25, f"learning:{new_id}")
372
- )
373
- get_db().commit()
374
- except Exception:
375
- pass # Somatic event logging is best-effort
376
-
377
- # Knowledge graph incremental population
378
- try:
379
- from kg_populate import on_learning_add
380
- on_learning_add(new_id, category, title, applies_to)
381
- except Exception:
382
- pass
383
-
384
- if supersedes_id:
385
- superseded = supersede_learning(int(supersedes_id), new_id, f"Superseded by learning #{new_id}.")
386
- if "error" in superseded:
387
- return f"ERROR: Learning #{new_id} created but supersede failed: {superseded['error']}"
388
-
389
- # Post-insert verification: confirm the learning actually persisted
390
- verify_conn = get_db()
391
- verified = verify_conn.execute(
392
- "SELECT id, title, category FROM learnings WHERE id = ? AND status = 'active'",
393
- (result["id"],)
394
- ).fetchone()
395
- if not verified:
396
- return (
397
- f"⚠ PERSISTENCE FAILURE: Learning #{result['id']} was inserted but NOT found on verification read. "
398
- f"Retry nexo_learning_add or investigate DB integrity."
399
- )
400
-
401
- # Fase 2 item 3: when a learning lands with an enforceable prevention
402
- # rule, scan recent decisions for ones that would have been decided
403
- # differently and surface them as deterministic followups. Best-effort:
404
- # a failure here must NEVER block the learning insert that the user
405
- # already verified above.
406
- retro_meta_msg = ""
407
- if prevention:
408
- try:
409
- from retroactive_learnings import apply_learning_retroactively
410
- retro_result = apply_learning_retroactively(
411
- int(result["id"]),
412
- lookback_days=14,
413
- max_matches=5,
414
- min_score=0.4,
415
- )
416
- if retro_result.get("followups_created"):
417
- retro_meta_msg = (
418
- f"\n📜 Retroactive scan: {retro_result['followups_created']} "
419
- f"past decision(s) flagged for review (scanned "
420
- f"{retro_result.get('scanned', 0)})"
421
- )
422
- except Exception:
423
- pass # Best-effort surfacing only
424
-
425
- meta = []
426
- if prevention:
427
- meta.append("with prevention")
428
- if applies_to:
429
- meta.append(f"applies_to={applies_to}")
430
- if supersedes_id:
431
- meta.append(f"supersedes={int(supersedes_id)}")
432
- meta_str = f" ({', '.join(meta)})" if meta else ""
433
- return f"Learning #{result['id']} added in {category}: {title}{meta_str} ✓verified{repetition_msg}{retro_meta_msg}"
434
-
435
-
436
- def handle_learning_search(query: str, category: str = '') -> str:
437
- """Search learnings by query string, optionally filtered by category."""
438
- results = search_learnings(query, category if category else None)
439
- if not results:
440
- return f"No results for '{query}'."
441
- lines = [f"RESULTS ({len(results)}):"]
442
- conn = get_db()
443
- for r in results:
444
- snippet = r["content"][:100] + "..." if len(r["content"]) > 100 else r["content"]
445
- status = r.get("status", "active")
446
- review_due = r.get("review_due_at")
447
- review_note = f" | review_due={review_due:.0f}" if isinstance(review_due, (int, float)) and review_due else ""
448
- pri = r.get("priority", "medium") or "medium"
449
- w = r.get("weight", 0.5) or 0.5
450
- quality = score_learning_quality(r, conn)
451
- pri_icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "⚪"}.get(pri, "🟡")
452
- lines.append(f" #{r['id']} [{r['category']}] [{status}] {pri_icon}{pri} w={w:.2f} q={quality['score']} {r['title']}{review_note}")
453
- lines.append(f" {snippet}")
454
- if r.get("prevention"):
455
- lines.append(f" Prevention: {r['prevention'][:100]}")
456
-
457
- # v1.2: Passive rehearsal — strengthen matching cognitive memories
458
- try:
459
- import cognitive
460
- for r in results[:5]:
461
- cognitive.rehearse_by_content(f"{r.get('title', '')} {r.get('content', '')[:200]}")
462
- except Exception:
463
- pass
464
-
465
- return "\n".join(lines)
466
-
467
-
468
- def handle_learning_update(id: int, title: str = '', content: str = '', category: str = '',
469
- reasoning: str = '', prevention: str = '', applies_to: str = '',
470
- status: str = '', review_days: int = 0, priority: str = '',
471
- supersedes_id: int = 0) -> str:
472
- """Update an existing learning, including review metadata and priority."""
473
- conn = get_db()
474
- current = conn.execute("SELECT * FROM learnings WHERE id = ?", (id,)).fetchone()
475
- if not current:
476
- return f"ERROR: Learning #{id} not found."
477
- kwargs = {}
478
- if title:
479
- kwargs["title"] = title
480
- if content:
481
- kwargs["content"] = content
482
- if category:
483
- kwargs["category"] = category.lower().strip()
484
- if reasoning:
485
- kwargs["reasoning"] = reasoning
486
- if prevention:
487
- kwargs["prevention"] = prevention
488
- if applies_to:
489
- kwargs["applies_to"] = applies_to
490
- if status:
491
- kwargs["status"] = status
492
- if review_days > 0:
493
- kwargs["review_days"] = review_days
494
- if not kwargs:
495
- return "ERROR: Nothing to update. Provide new fields."
496
- effective_category = kwargs.get("category", current["category"])
497
- effective_title = kwargs.get("title", current["title"])
498
- effective_content = kwargs.get("content", current["content"])
499
- effective_applies_to = kwargs.get("applies_to", current["applies_to"])
500
- effective_status = kwargs.get("status", current["status"])
501
- if effective_status != "superseded":
502
- conflicting = _find_conflicting_active_learning(
503
- conn,
504
- category=effective_category,
505
- title=effective_title,
506
- content=effective_content,
507
- applies_to=effective_applies_to,
508
- exclude_id=id,
509
- )
510
- if conflicting and int(supersedes_id or 0) != int(conflicting["id"]):
511
- return (
512
- f"ERROR: Update would conflict with active learning #{conflicting['id']} "
513
- f"for applies_to={conflicting.get('applies_to', '')}. "
514
- f"Supersede the old rule or merge into one canonical learning."
515
- )
516
- basic_kwargs = {k: v for k, v in kwargs.items() if k in {"title", "content", "category", "reasoning"}}
517
- result = update_learning(id, **basic_kwargs)
518
- if "error" in result:
519
- return f"ERROR: {result['error']}"
520
- extra_updates = {}
521
- if prevention:
522
- extra_updates["prevention"] = prevention
523
- if applies_to:
524
- extra_updates["applies_to"] = applies_to
525
- if status:
526
- extra_updates["status"] = status
527
- if priority and priority in ('critical', 'high', 'medium', 'low'):
528
- extra_updates["priority"] = priority
529
- extra_updates["weight"] = {'critical': 0.9, 'high': 0.7, 'medium': 0.5, 'low': 0.3}[priority]
530
- if review_days > 0:
531
- extra_updates["review_due_at"] = now_epoch() + (max(1, int(review_days)) * 86400)
532
- if extra_updates:
533
- extra_updates["updated_at"] = now_epoch()
534
- set_clause = ", ".join(f"{k} = ?" for k in extra_updates)
535
- values = list(extra_updates.values()) + [id]
536
- conn = get_db()
537
- conn.execute(f"UPDATE learnings SET {set_clause} WHERE id = ?", values)
538
- conn.commit()
539
- if supersedes_id:
540
- superseded = supersede_learning(int(supersedes_id), id, f"Superseded by learning #{id}.")
541
- if "error" in superseded:
542
- return f"ERROR: Learning #{id} updated but supersede failed: {superseded['error']}"
543
- return f"Learning #{id} updated."
544
-
545
-
546
- def handle_learning_delete(id: int) -> str:
547
- """Delete a learning entry by ID."""
548
- deleted = delete_learning(id)
549
- if not deleted:
550
- return f"ERROR: Learning #{id} not found."
551
- return f"Learning #{id} deleted."
552
-
553
-
554
- def handle_learning_list(category: str = '') -> str:
555
- """List all learnings, grouped by category if no filter given."""
556
- results = list_learnings(category if category else None)
557
- if not results:
558
- label = category if category else "ALL"
559
- return f"LEARNINGS {label} (0): No entries."
560
-
561
- conn = get_db()
562
- if category:
563
- label = category.upper()
564
- lines = [f"LEARNINGS {label} ({len(results)}):"]
565
- for r in results:
566
- pri = r.get("priority", "medium") or "medium"
567
- w = r.get("weight", 0.5) or 0.5
568
- quality = score_learning_quality(r, conn)
569
- pri_icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "⚪"}.get(pri, "🟡")
570
- lines.append(f" #{r['id']} [{r.get('status','active')}] {pri_icon}{pri} w={w:.2f} q={quality['score']} {r['title']}")
571
- else:
572
- lines = [f"LEARNINGS ALL ({len(results)}):"]
573
- current_cat = None
574
- for r in results:
575
- if r["category"] != current_cat:
576
- current_cat = r["category"]
577
- lines.append(f"\n [{current_cat.upper()}]")
578
- pri = r.get("priority", "medium") or "medium"
579
- w = r.get("weight", 0.5) or 0.5
580
- quality = score_learning_quality(r, conn)
581
- pri_icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "⚪"}.get(pri, "🟡")
582
- lines.append(f" #{r['id']} [{r.get('status','active')}] {pri_icon}{pri} w={w:.2f} q={quality['score']} {r['title']}")
583
-
584
- return "\n".join(lines)
585
-
586
-
587
- def handle_learning_quality(id: int = 0, category: str = "", status: str = "active", limit: int = 20) -> str:
588
- """Inspect memory quality so fragile learnings can be tightened before they mislead guard/retrieval."""
589
- results = list_learnings(category if category else None)
590
- if id:
591
- results = [row for row in results if int(row.get("id") or 0) == int(id)]
592
- if status:
593
- results = [row for row in results if str(row.get("status") or "").lower() == str(status).lower()]
594
- results = results[: max(1, int(limit or 20))]
595
- if not results:
596
- return "LEARNING QUALITY (0): No matching learnings."
597
-
598
- conn = get_db()
599
- scored = []
600
- for row in results:
601
- quality = score_learning_quality(row, conn)
602
- scored.append((row, quality))
603
- avg_score = round(sum(item[1]["score"] for item in scored) / len(scored))
604
- weak = [item for item in scored if item[1]["score"] < 60]
605
- lines = [f"LEARNING QUALITY ({len(scored)}) avg={avg_score} weak={len(weak)}:"]
606
- for row, quality in scored:
607
- lines.append(
608
- f" #{row['id']} q={quality['score']} [{quality['label']}] {row['title']} "
609
- f"(conf={quality['confidence']} useful={quality['usefulness']} recency={quality['recency_relevance']} "
610
- f"pressure={quality['contradiction_pressure']} richness={quality['source_richness']})"
611
- )
612
- return "\n".join(lines)