nexo-brain 5.3.26 → 5.3.27

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/package.json +1 -1
  3. package/src/server.py +3 -0
  4. package/src/tools_sessions.py +6 -1
  5. package/src/dashboard/static/favicon 2.svg +0 -32
  6. package/src/dashboard/static/nexo-logo 2.png +0 -0
  7. package/src/dashboard/static/nexo-logo 2.svg +0 -40
  8. package/src/dashboard/static/style 2.css +0 -2458
  9. package/src/dashboard/templates/adaptive 2.html +0 -118
  10. package/src/dashboard/templates/artifacts 2.html +0 -133
  11. package/src/dashboard/templates/backups 2.html +0 -136
  12. package/src/dashboard/templates/base 2.html +0 -417
  13. package/src/dashboard/templates/calendar 2.html +0 -591
  14. package/src/dashboard/templates/chat 2.html +0 -356
  15. package/src/dashboard/templates/claims 2.html +0 -259
  16. package/src/dashboard/templates/cortex 2.html +0 -321
  17. package/src/dashboard/templates/credentials 2.html +0 -128
  18. package/src/dashboard/templates/crons 2.html +0 -370
  19. package/src/dashboard/templates/dashboard 2.html +0 -494
  20. package/src/dashboard/templates/dreams 2.html +0 -252
  21. package/src/dashboard/templates/email 2.html +0 -160
  22. package/src/dashboard/templates/evolution 2.html +0 -189
  23. package/src/dashboard/templates/feed 2.html +0 -249
  24. package/src/dashboard/templates/followup_health 2.html +0 -170
  25. package/src/dashboard/templates/graph 2.html +0 -201
  26. package/src/dashboard/templates/guard 2.html +0 -259
  27. package/src/dashboard/templates/inbox 2.html +0 -251
  28. package/src/dashboard/templates/memory 2.html +0 -420
  29. package/src/dashboard/templates/operations 2.html +0 -608
  30. package/src/dashboard/templates/plugins 2.html +0 -185
  31. package/src/dashboard/templates/protocol 2.html +0 -199
  32. package/src/dashboard/templates/rules 2.html +0 -246
  33. package/src/dashboard/templates/sentiment 2.html +0 -247
  34. package/src/dashboard/templates/sessions 2.html +0 -218
  35. package/src/dashboard/templates/skills 2.html +0 -329
  36. package/src/dashboard/templates/somatic 2.html +0 -73
  37. package/src/dashboard/templates/triggers 2.html +0 -133
  38. package/src/dashboard/templates/trust 2.html +0 -360
  39. package/src/db/__init__ 2.py +0 -259
  40. package/src/db/_core 2.py +0 -437
  41. package/src/db/_credentials 2.py +0 -124
  42. package/src/db/_episodic 2.py +0 -762
  43. package/src/db/_evolution 2.py +0 -54
  44. package/src/db/_fts 2.py +0 -406
  45. package/src/db/_goal_profiles 2.py +0 -376
  46. package/src/db/_hot_context 2.py +0 -660
  47. package/src/db/_outcomes 2.py +0 -800
  48. package/src/db/_personal_scripts 2.py +0 -582
  49. package/src/db/_sessions 2.py +0 -330
  50. package/src/db/_tasks 2.py +0 -91
  51. package/src/db/_watchers 2.py +0 -173
  52. package/src/doctor/formatters 2.py +0 -52
  53. package/src/doctor/models 2.py +0 -69
  54. package/src/doctor/planes 2.py +0 -87
  55. package/src/doctor/providers/__init__ 2.py +0 -1
  56. package/src/doctor/providers/deep 2.py +0 -367
  57. package/src/evolution_cycle 2.py +0 -519
  58. package/src/hooks/auto_capture 2.py +0 -208
  59. package/src/hooks/caffeinate-guard 2.sh +0 -8
  60. package/src/hooks/capture-session 2.sh +0 -21
  61. package/src/hooks/capture-tool-logs 2.sh +0 -158
  62. package/src/hooks/daily-briefing-check 2.sh +0 -33
  63. package/src/hooks/heartbeat-enforcement 2.py +0 -90
  64. package/src/hooks/heartbeat-posttool 2.sh +0 -18
  65. package/src/hooks/inbox-hook 2.sh +0 -76
  66. package/src/hooks/post-compact 2.sh +0 -152
  67. package/src/hooks/pre-compact 2.sh +0 -169
  68. package/src/hooks/protocol-guardrail 2.sh +0 -10
  69. package/src/hooks/protocol-pretool-guardrail 2.sh +0 -9
  70. package/src/hooks/session-stop 2.sh +0 -52
  71. package/src/kg_populate 2.py +0 -292
  72. package/src/maintenance 2.py +0 -53
  73. package/src/memory_backends 2.py +0 -71
  74. package/src/migrate_embeddings 2.py +0 -124
  75. package/src/nexo_sdk 2.py +0 -103
  76. package/src/observability 2.py +0 -199
  77. package/src/plugin_loader 2.py +0 -217
  78. package/src/plugins/__init__ 2.py +0 -0
  79. package/src/plugins/artifact_registry 2.py +0 -450
  80. package/src/plugins/backup 2.py +0 -127
  81. package/src/plugins/claims_tools 2.py +0 -119
  82. package/src/plugins/cognitive_memory 2.py +0 -609
  83. package/src/plugins/core_rules 2.py +0 -252
  84. package/src/plugins/cortex 2.py +0 -1155
  85. package/src/plugins/entities 2.py +0 -67
  86. package/src/plugins/episodic_memory 2.py +0 -560
  87. package/src/plugins/evolution 2.py +0 -167
  88. package/src/plugins/goal_engine 2.py +0 -142
  89. package/src/plugins/guard 2.py +0 -862
  90. package/src/plugins/impact 2.py +0 -29
  91. package/src/plugins/knowledge_graph_tools 2.py +0 -137
  92. package/src/plugins/media_memory_tools 2.py +0 -98
  93. package/src/plugins/memory_export 2.py +0 -196
  94. package/src/plugins/outcomes 2.py +0 -130
  95. package/src/plugins/personal_scripts 2.py +0 -117
  96. package/src/plugins/preferences 2.py +0 -47
  97. package/src/plugins/protocol 2.py +0 -1449
  98. package/src/plugins/simple_api 2.py +0 -106
  99. package/src/plugins/skills 2.py +0 -341
  100. package/src/plugins/state_watchers 2.py +0 -79
  101. package/src/plugins/update 2.py +0 -986
  102. package/src/plugins/user_state_tools 2.py +0 -43
  103. package/src/plugins/workflow 2.py +0 -588
  104. package/src/protocol_settings 2.py +0 -59
  105. package/src/public_contribution 2.py +0 -466
  106. package/src/public_evolution_queue 2.py +0 -241
  107. package/src/requirements 2.txt +0 -14
  108. package/src/retroactive_learnings 2.py +0 -373
  109. package/src/rules/__init__ 2.py +0 -0
  110. package/src/rules/core-rules 2.json +0 -331
  111. package/src/rules/migrate 2.py +0 -207
  112. package/src/runtime_power 2.py +0 -874
  113. package/src/script_registry 2.py +0 -1559
  114. package/src/scripts/check-context 2.py +0 -272
  115. package/src/scripts/deep-sleep/apply_findings 2.py +0 -2327
  116. package/src/scripts/deep-sleep/collect 2.py +0 -928
  117. package/src/scripts/deep-sleep/extract 2.py +0 -330
  118. package/src/scripts/deep-sleep/extract-prompt 2.md +0 -285
  119. package/src/scripts/deep-sleep/synthesize 2.py +0 -312
  120. package/src/scripts/deep-sleep/synthesize-prompt 2.md +0 -336
  121. package/src/scripts/nexo-agent-run 2.py +0 -75
  122. package/src/scripts/nexo-auto-update 2.py +0 -6
  123. package/src/scripts/nexo-backup 2.sh +0 -25
  124. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  125. package/src/scripts/nexo-catchup 2.py +0 -300
  126. package/src/scripts/nexo-cognitive-decay 2.py +0 -257
  127. package/src/scripts/nexo-cortex-cycle 2.py +0 -293
  128. package/src/scripts/nexo-cron-wrapper 2.sh +0 -53
  129. package/src/scripts/nexo-daily-self-audit 2.py +0 -2161
  130. package/src/scripts/nexo-dashboard 2.sh +0 -29
  131. package/src/scripts/nexo-deep-sleep 2.sh +0 -86
  132. package/src/scripts/nexo-evolution-run 2.py +0 -1664
  133. package/src/scripts/nexo-followup-hygiene 2.py +0 -139
  134. package/src/scripts/nexo-hook-record 2.py +0 -42
  135. package/src/scripts/nexo-immune 2.py +0 -936
  136. package/src/scripts/nexo-impact-scorer 2.py +0 -117
  137. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  138. package/src/scripts/nexo-install 2.py +0 -6
  139. package/src/scripts/nexo-learning-housekeep 2.py +0 -401
  140. package/src/scripts/nexo-learning-validator 2.py +0 -266
  141. package/src/scripts/nexo-migrate 2.py +0 -260
  142. package/src/scripts/nexo-outcome-checker 2.py +0 -127
  143. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -456
  144. package/src/scripts/nexo-pre-commit 2.py +0 -120
  145. package/src/scripts/nexo-prevent-sleep 2.sh +0 -35
  146. package/src/scripts/nexo-proactive-dashboard 2.py +0 -354
  147. package/src/scripts/nexo-reflection 2.py +0 -256
  148. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  149. package/src/scripts/nexo-sleep 2.py +0 -631
  150. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  151. package/src/scripts/nexo-sync-clients 2.py +0 -16
  152. package/src/scripts/nexo-synthesis 2.py +0 -475
  153. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  154. package/src/scripts/nexo-update 2.sh +0 -306
  155. package/src/scripts/nexo-watchdog 2.sh +0 -1207
  156. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  157. package/src/scripts/rehydrate_learnings_from_archive 2.py +0 -245
  158. package/src/server 2.py +0 -1296
  159. package/src/skills/run-nexo-audit-phase/guide 2.md +0 -43
  160. package/src/skills/run-nexo-audit-phase/skill 2.json +0 -59
  161. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +0 -17
  162. package/src/skills/run-nexo-core-fix-cycle/script 2.py +0 -276
  163. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +0 -58
  164. package/src/skills/run-release-final-audit/guide 2.md +0 -16
  165. package/src/skills/run-release-final-audit/script 2.py +0 -259
  166. package/src/skills/run-release-final-audit/skill 2.json +0 -77
  167. package/src/skills/run-runtime-doctor/guide 2.md +0 -12
  168. package/src/skills/run-runtime-doctor/script 2.py +0 -21
  169. package/src/skills/run-runtime-doctor/skill 2.json +0 -25
  170. package/src/skills_runtime 2.py +0 -932
  171. package/src/state_watchers_runtime 2.py +0 -475
  172. package/src/storage_router 2.py +0 -32
  173. package/src/system_catalog 2.py +0 -786
  174. package/src/tools_coordination 2.py +0 -103
  175. package/src/tools_credentials 2.py +0 -68
  176. package/src/tools_drive 2.py +0 -487
  177. package/src/tools_hot_context 2.py +0 -163
  178. package/src/tools_learnings 2.py +0 -612
  179. package/src/tools_menu 2.py +0 -229
  180. package/src/tools_reminders 2.py +0 -88
  181. package/src/tools_reminders_crud 2.py +0 -363
  182. package/src/tools_sessions 2.py +0 -1054
  183. package/src/tools_system_catalog 2.py +0 -19
  184. package/src/tools_task_history 2.py +0 -57
  185. package/src/tools_transcripts 2.py +0 -98
  186. package/src/transcript_utils 2.py +0 -412
  187. package/src/user_context 2.py +0 -46
  188. package/src/user_data_portability 2.py +0 -328
  189. package/src/user_state_model 2.py +0 -170
  190. package/templates/CLAUDE.md 2.template +0 -108
  191. package/templates/CODEX.AGENTS.md 2.template +0 -66
  192. package/templates/launchagents/README 2.md +0 -132
  193. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +0 -39
  194. package/templates/launchagents/com.nexo.catchup 2.plist +0 -39
  195. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +0 -40
  196. package/templates/launchagents/com.nexo.dashboard 2.plist +0 -43
  197. package/templates/launchagents/com.nexo.deep-sleep 2.plist +0 -43
  198. package/templates/launchagents/com.nexo.evolution 2.plist +0 -44
  199. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +0 -45
  200. package/templates/launchagents/com.nexo.immune 2.plist +0 -41
  201. package/templates/launchagents/com.nexo.postmortem 2.plist +0 -45
  202. package/templates/launchagents/com.nexo.self-audit 2.plist +0 -47
  203. package/templates/launchagents/com.nexo.synthesis 2.plist +0 -45
  204. package/templates/launchagents/com.nexo.watchdog 2.plist +0 -37
  205. package/templates/nexo_helper 2.py +0 -301
  206. package/templates/openclaw 2.json +0 -13
  207. package/templates/plugin-template 2.py +0 -40
  208. package/templates/script-template 2.py +0 -59
  209. package/templates/script-template 2.sh +0 -13
  210. package/templates/skill-script-template 2.py +0 -48
  211. 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)