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,800 +0,0 @@
1
- from __future__ import annotations
2
- """NEXO DB — Outcome tracker v1."""
3
-
4
- import datetime
5
- import json
6
- from typing import Any
7
-
8
- from db._core import get_db
9
-
10
- VALID_METRIC_SOURCES = {
11
- "manual",
12
- "followup_status",
13
- "decision_outcome",
14
- "protocol_task_status",
15
- "nexo_sqlite",
16
- }
17
- VALID_TARGET_OPERATORS = {"gte", "lte", "eq"}
18
- OUTCOME_PATTERN_MIN_RESOLVED = 3
19
- OUTCOME_PATTERN_MAX_EVIDENCE = 5
20
- OUTCOME_PATTERN_LEARNING_SUCCESS_BOOST = 0.9
21
- OUTCOME_PATTERN_LEARNING_RISK_REDUCTION = 0.6
22
- OUTCOME_PATTERN_LEARNING_SUCCESS_PENALTY = -1.1
23
- OUTCOME_PATTERN_LEARNING_RISK_PENALTY = 0.8
24
- OUTCOME_PATTERN_MIN_SUCCESS_RATE = 0.75
25
- OUTCOME_PATTERN_MAX_FAILURE_RATE = 0.25
26
-
27
-
28
- def _utcnow_iso() -> str:
29
- return datetime.datetime.now().isoformat(timespec="seconds")
30
-
31
-
32
- def _normalize_deadline(deadline: str = "", *, default_days: int = 7) -> str:
33
- clean = (deadline or "").strip()
34
- if clean:
35
- return clean
36
- return (datetime.datetime.now() + datetime.timedelta(days=default_days)).isoformat(timespec="seconds")
37
-
38
-
39
- def _normalize_source(metric_source: str) -> str:
40
- clean = (metric_source or "manual").strip().lower()
41
- if clean not in VALID_METRIC_SOURCES:
42
- raise ValueError(f"metric_source must be one of: {', '.join(sorted(VALID_METRIC_SOURCES))}")
43
- return clean
44
-
45
-
46
- def _normalize_operator(target_operator: str) -> str:
47
- clean = (target_operator or "gte").strip().lower()
48
- if clean not in VALID_TARGET_OPERATORS:
49
- raise ValueError(f"target_operator must be one of: {', '.join(sorted(VALID_TARGET_OPERATORS))}")
50
- return clean
51
-
52
-
53
- def _as_float(value: Any) -> float | None:
54
- if value is None or value == "":
55
- return None
56
- try:
57
- return float(value)
58
- except (TypeError, ValueError):
59
- return None
60
-
61
-
62
- def _append_note(existing: str | None, extra: str) -> str:
63
- extra = (extra or "").strip()
64
- if not extra:
65
- return (existing or "").strip()
66
- existing = (existing or "").strip()
67
- if not existing:
68
- return extra
69
- if extra in existing:
70
- return existing
71
- return f"{existing}\n{extra}"
72
-
73
-
74
- def _format_scalar(value: Any) -> str:
75
- if value is None:
76
- return ""
77
- if isinstance(value, float):
78
- return f"{value:.4f}".rstrip("0").rstrip(".")
79
- return str(value)
80
-
81
-
82
- def _pattern_key(*, area: str, task_type: str, goal_profile_id: str, selected_choice: str) -> str:
83
- return json.dumps(
84
- {
85
- "area": (area or "").strip(),
86
- "task_type": (task_type or "").strip(),
87
- "goal_profile_id": (goal_profile_id or "").strip(),
88
- "selected_choice": (selected_choice or "").strip(),
89
- },
90
- ensure_ascii=False,
91
- sort_keys=True,
92
- )
93
-
94
-
95
- def _context_label(*, area: str, task_type: str, goal_profile_id: str) -> str:
96
- bits = []
97
- if (area or "").strip():
98
- bits.append(f"area={area.strip()}")
99
- if (task_type or "").strip():
100
- bits.append(f"task_type={task_type.strip()}")
101
- if (goal_profile_id or "").strip():
102
- bits.append(f"profile={goal_profile_id.strip()}")
103
- return ", ".join(bits) if bits else "contexto general"
104
-
105
-
106
- def _compare(actual_value: float, target_value: float, operator: str) -> bool:
107
- if operator == "gte":
108
- return actual_value >= target_value
109
- if operator == "lte":
110
- return actual_value <= target_value
111
- return actual_value == target_value
112
-
113
-
114
- def _get_outcome_row(conn, outcome_id: int):
115
- return conn.execute("SELECT * FROM outcomes WHERE id = ?", (int(outcome_id),)).fetchone()
116
-
117
-
118
- def _update_outcome(
119
- outcome_id: int,
120
- *,
121
- status: str | None = None,
122
- actual_value: float | None = None,
123
- actual_value_text: str | None = None,
124
- checked_at: str | None = None,
125
- notes: str | None = None,
126
- learning_id: int | None = None,
127
- ) -> dict:
128
- conn = get_db()
129
- row = _get_outcome_row(conn, outcome_id)
130
- if not row:
131
- return {"error": f"Outcome {outcome_id} not found"}
132
-
133
- updates: list[str] = []
134
- params: list[Any] = []
135
-
136
- if status is not None:
137
- updates.append("status = ?")
138
- params.append(status)
139
- if actual_value is not None:
140
- updates.append("actual_value = ?")
141
- params.append(actual_value)
142
- if actual_value_text is not None:
143
- updates.append("actual_value_text = ?")
144
- params.append(actual_value_text.strip())
145
- if checked_at is not None:
146
- updates.append("checked_at = ?")
147
- params.append(checked_at)
148
- if notes is not None:
149
- updates.append("notes = ?")
150
- params.append(notes)
151
- if learning_id is not None:
152
- updates.append("learning_id = ?")
153
- params.append(learning_id)
154
-
155
- updates.append("updated_at = datetime('now')")
156
- params.append(int(outcome_id))
157
- conn.execute(f"UPDATE outcomes SET {', '.join(updates)} WHERE id = ?", params)
158
- conn.commit()
159
- row = _get_outcome_row(conn, outcome_id)
160
- return dict(row) if row else {"error": f"Outcome {outcome_id} not found after update"}
161
-
162
-
163
- def create_outcome(
164
- action_type: str,
165
- description: str,
166
- expected_result: str,
167
- *,
168
- metric_source: str = "manual",
169
- metric_query: str = "",
170
- baseline_value: float | None = None,
171
- target_value: float | None = None,
172
- target_operator: str = "gte",
173
- deadline: str = "",
174
- action_id: str = "",
175
- session_id: str = "",
176
- notes: str = "",
177
- ) -> dict:
178
- conn = get_db()
179
- clean_action_type = (action_type or "").strip()
180
- clean_description = (description or "").strip()
181
- clean_expected = (expected_result or "").strip()
182
- if not clean_action_type:
183
- return {"error": "action_type is required"}
184
- if not clean_description:
185
- return {"error": "description is required"}
186
- if not clean_expected:
187
- return {"error": "expected_result is required"}
188
-
189
- try:
190
- clean_source = _normalize_source(metric_source)
191
- clean_operator = _normalize_operator(target_operator)
192
- except ValueError as exc:
193
- return {"error": str(exc)}
194
-
195
- clean_query = (metric_query or "").strip()
196
- if clean_source in {"followup_status", "decision_outcome", "protocol_task_status"} and not (action_id or "").strip():
197
- return {"error": f"action_id is required for metric_source='{clean_source}'"}
198
- if clean_source == "nexo_sqlite":
199
- if not clean_query:
200
- return {"error": "metric_query is required for metric_source='nexo_sqlite'"}
201
- query_upper = clean_query.upper()
202
- if ";" in clean_query.rstrip(";"):
203
- return {"error": "metric_query must be a single SELECT statement"}
204
- if not query_upper.startswith("SELECT "):
205
- return {"error": "metric_query must start with SELECT"}
206
- for forbidden in ("INSERT ", "UPDATE ", "DELETE ", "DROP ", "ALTER ", "ATTACH ", "DETACH ", "PRAGMA "):
207
- if forbidden in query_upper:
208
- return {"error": "metric_query must be read-only"}
209
-
210
- cursor = conn.execute(
211
- """INSERT INTO outcomes (
212
- action_type, action_id, session_id, description, expected_result,
213
- metric_source, metric_query, baseline_value, target_value,
214
- target_operator, deadline, notes
215
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
216
- (
217
- clean_action_type,
218
- (action_id or "").strip(),
219
- (session_id or "").strip(),
220
- clean_description,
221
- clean_expected,
222
- clean_source,
223
- clean_query,
224
- baseline_value,
225
- target_value,
226
- clean_operator,
227
- _normalize_deadline(deadline),
228
- (notes or "").strip(),
229
- ),
230
- )
231
- conn.commit()
232
- row = _get_outcome_row(conn, cursor.lastrowid)
233
- return dict(row) if row else {"error": "Outcome insert failed"}
234
-
235
-
236
- def get_outcome(outcome_id: int) -> dict | None:
237
- conn = get_db()
238
- row = _get_outcome_row(conn, outcome_id)
239
- return dict(row) if row else None
240
-
241
-
242
- def list_outcomes(status: str = "", action_type: str = "", limit: int = 50) -> list[dict]:
243
- conn = get_db()
244
- clauses = []
245
- params: list[Any] = []
246
- if status:
247
- clauses.append("status = ?")
248
- params.append((status or "").strip().lower())
249
- if action_type:
250
- clauses.append("action_type = ?")
251
- params.append((action_type or "").strip())
252
- where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
253
- rows = conn.execute(
254
- f"""SELECT * FROM outcomes
255
- {where}
256
- ORDER BY
257
- CASE status
258
- WHEN 'pending' THEN 0
259
- WHEN 'missed' THEN 1
260
- WHEN 'met' THEN 2
261
- ELSE 3
262
- END,
263
- deadline ASC,
264
- created_at DESC
265
- LIMIT ?""",
266
- params + [max(1, int(limit))],
267
- ).fetchall()
268
- return [dict(row) for row in rows]
269
-
270
-
271
- def pending_outcomes_due(deadline_before: str | None = None, limit: int = 100) -> list[dict]:
272
- conn = get_db()
273
- cutoff = deadline_before or _utcnow_iso()
274
- rows = conn.execute(
275
- """SELECT * FROM outcomes
276
- WHERE status = 'pending' AND deadline <= ?
277
- ORDER BY deadline ASC, id ASC
278
- LIMIT ?""",
279
- (cutoff, max(1, int(limit))),
280
- ).fetchall()
281
- return [dict(row) for row in rows]
282
-
283
-
284
- def find_pending_outcomes_by_action(action_type: str, action_id: str, *, metric_source: str = "") -> list[dict]:
285
- conn = get_db()
286
- clauses = [
287
- "status = 'pending'",
288
- "action_type = ?",
289
- "action_id = ?",
290
- ]
291
- params: list[Any] = [(action_type or "").strip(), (action_id or "").strip()]
292
- if metric_source:
293
- clauses.append("metric_source = ?")
294
- params.append((metric_source or "").strip().lower())
295
- rows = conn.execute(
296
- f"SELECT * FROM outcomes WHERE {' AND '.join(clauses)} ORDER BY deadline ASC, id ASC",
297
- params,
298
- ).fetchall()
299
- return [dict(row) for row in rows]
300
-
301
-
302
- def cancel_outcome(outcome_id: int, reason: str = "") -> dict:
303
- conn = get_db()
304
- row = _get_outcome_row(conn, outcome_id)
305
- if not row:
306
- return {"error": f"Outcome {outcome_id} not found"}
307
- notes = _append_note(row["notes"], f"Cancelled: {(reason or '').strip() or 'no reason provided'}")
308
- return _update_outcome(
309
- int(outcome_id),
310
- status="cancelled",
311
- checked_at=_utcnow_iso(),
312
- notes=notes,
313
- )
314
-
315
-
316
- def _create_miss_learning(row: dict, actual_value: float | None, actual_value_text: str, note: str) -> int | None:
317
- if row.get("learning_id"):
318
- return int(row["learning_id"])
319
- from db._learnings import create_learning
320
-
321
- summary_bits = [
322
- f"Outcome #{row['id']} missed.",
323
- f"Action type: {row.get('action_type', '')}.",
324
- f"Action id: {row.get('action_id', '') or 'N/A'}.",
325
- f"Description: {row.get('description', '')}.",
326
- f"Expected: {row.get('expected_result', '')}.",
327
- ]
328
- if actual_value is not None:
329
- summary_bits.append(f"Actual numeric value: {actual_value}.")
330
- if actual_value_text:
331
- summary_bits.append(f"Actual evidence: {actual_value_text}.")
332
- if note:
333
- summary_bits.append(f"Why missed: {note}.")
334
- learning = create_learning(
335
- category="outcomes",
336
- title=f"Outcome missed: {str(row.get('description', ''))[:80]}",
337
- content=" ".join(summary_bits),
338
- reasoning=f"Auto-created from missed outcome #{row['id']}.",
339
- prevention="Review the action, expected result, target, or deadline before repeating the same move.",
340
- applies_to=f"outcome:{row['id']}",
341
- )
342
- return int(learning["id"]) if learning and learning.get("id") else None
343
-
344
-
345
- def _status_from_protocol_task(task_row: dict | None, *, deadline_passed: bool) -> tuple[str, float | None, str, str]:
346
- if not task_row:
347
- if deadline_passed:
348
- return "missed", None, "protocol task missing", "Linked protocol task not found."
349
- return "pending", None, "", ""
350
- status = str(task_row.get("status") or "").strip().lower()
351
- if status == "done":
352
- evidence = (task_row.get("outcome_notes") or task_row.get("close_evidence") or task_row.get("goal") or "").strip()
353
- return "met", 1.0, evidence, "Linked protocol task closed as done."
354
- if status in {"failed", "cancelled"}:
355
- evidence = (task_row.get("outcome_notes") or task_row.get("close_evidence") or status).strip()
356
- return "missed", 0.0, evidence, f"Linked protocol task closed as {status}."
357
- if deadline_passed:
358
- return "missed", None, status or "open", f"Deadline passed while linked protocol task remained {status or 'open'}."
359
- return "pending", None, status, ""
360
-
361
-
362
- def evaluate_outcome(
363
- outcome_id: int,
364
- *,
365
- actual_value: float | None = None,
366
- actual_value_text: str = "",
367
- create_learning_on_miss: bool = True,
368
- ) -> dict:
369
- conn = get_db()
370
- row = _get_outcome_row(conn, outcome_id)
371
- if not row:
372
- return {"error": f"Outcome {outcome_id} not found"}
373
- row_d = dict(row)
374
- if row_d.get("status") != "pending":
375
- row_d["evaluation"] = "skipped_non_pending"
376
- return row_d
377
-
378
- now_iso = _utcnow_iso()
379
- deadline_passed = str(row_d.get("deadline") or "") <= now_iso
380
- source = (row_d.get("metric_source") or "manual").strip().lower()
381
- target = _as_float(row_d.get("target_value"))
382
- operator = (row_d.get("target_operator") or "gte").strip().lower()
383
-
384
- status = "pending"
385
- actual_num = _as_float(actual_value)
386
- actual_text = (actual_value_text or "").strip()
387
- note = ""
388
-
389
- if source == "manual":
390
- if actual_num is not None:
391
- actual_text = actual_text or _format_scalar(actual_num)
392
- if target is None:
393
- status = "met"
394
- note = "Manual evidence recorded."
395
- elif _compare(actual_num, target, operator):
396
- status = "met"
397
- note = f"Manual check met target via operator '{operator}'."
398
- elif deadline_passed:
399
- status = "missed"
400
- note = f"Manual check below target and deadline passed (actual={actual_num}, target={target}, op={operator})."
401
- else:
402
- note = f"Manual check recorded current value (actual={actual_num}, target={target}, op={operator})."
403
- elif actual_text:
404
- status = "met"
405
- note = "Manual textual evidence recorded."
406
- elif deadline_passed:
407
- status = "missed"
408
- note = "Deadline passed with no manual evidence recorded."
409
-
410
- elif source == "followup_status":
411
- followup = conn.execute(
412
- "SELECT status, description, verification FROM followups WHERE id = ?",
413
- (row_d.get("action_id", ""),),
414
- ).fetchone()
415
- followup_status = str(followup["status"]) if followup else ""
416
- if followup and followup_status.upper().startswith("COMPLETED"):
417
- status = "met"
418
- actual_num = 1.0
419
- actual_text = (followup["verification"] or followup["description"] or followup_status or "").strip()
420
- note = "Linked followup completed."
421
- elif deadline_passed:
422
- status = "missed"
423
- actual_text = actual_text or (followup_status or "missing")
424
- note = f"Deadline passed while linked followup remained {followup_status or 'missing'}."
425
-
426
- elif source == "decision_outcome":
427
- decision = conn.execute(
428
- "SELECT outcome, status, decision FROM decisions WHERE id = ?",
429
- (row_d.get("action_id", ""),),
430
- ).fetchone()
431
- decision_outcome = (decision["outcome"] or "").strip() if decision else ""
432
- if decision and decision_outcome:
433
- status = "met"
434
- actual_num = 1.0
435
- actual_text = decision_outcome
436
- note = "Linked decision outcome recorded."
437
- elif deadline_passed:
438
- status = "missed"
439
- actual_text = actual_text or (str(decision["status"]) if decision else "missing")
440
- note = f"Deadline passed with no linked decision outcome for decision {row_d.get('action_id', '') or 'N/A'}."
441
-
442
- elif source == "protocol_task_status":
443
- task = conn.execute(
444
- "SELECT status, goal, close_evidence, outcome_notes FROM protocol_tasks WHERE task_id = ?",
445
- (row_d.get("action_id", ""),),
446
- ).fetchone()
447
- status, actual_num, actual_text, note = _status_from_protocol_task(dict(task) if task else None, deadline_passed=deadline_passed)
448
-
449
- elif source == "nexo_sqlite":
450
- query = (row_d.get("metric_query") or "").strip()
451
- if not query:
452
- return {"error": f"Outcome {outcome_id} has empty metric_query"}
453
- if ";" in query.rstrip(";") or not query.upper().startswith("SELECT "):
454
- return {"error": "Outcome metric_query must be a single SELECT statement"}
455
- fetched = conn.execute(query).fetchone()
456
- scalar = fetched[0] if fetched else None
457
- actual_num = _as_float(scalar)
458
- actual_text = _format_scalar(scalar)
459
- if actual_num is None:
460
- if deadline_passed:
461
- status = "missed"
462
- note = f"SQLite query did not return a numeric scalar before deadline. Got: {actual_text or 'empty'}."
463
- else:
464
- note = f"SQLite query returned non-numeric scalar: {actual_text or 'empty'}."
465
- elif target is None:
466
- if actual_num:
467
- status = "met"
468
- note = f"SQLite query returned truthy scalar {actual_num}."
469
- elif deadline_passed:
470
- status = "missed"
471
- note = "SQLite query returned falsy scalar and deadline passed."
472
- else:
473
- note = f"SQLite query current scalar is {actual_num}."
474
- elif _compare(actual_num, target, operator):
475
- status = "met"
476
- note = f"SQLite scalar met target (actual={actual_num}, target={target}, op={operator})."
477
- elif deadline_passed:
478
- status = "missed"
479
- note = f"SQLite scalar missed target at deadline (actual={actual_num}, target={target}, op={operator})."
480
- else:
481
- note = f"SQLite scalar recorded but target not reached yet (actual={actual_num}, target={target}, op={operator})."
482
-
483
- learning_id = row_d.get("learning_id")
484
- combined_notes = _append_note(row_d.get("notes"), note)
485
- if status == "missed" and create_learning_on_miss:
486
- learning_id = _create_miss_learning(row_d, actual_num, actual_text, note)
487
-
488
- updated = _update_outcome(
489
- int(outcome_id),
490
- status=status,
491
- actual_value=actual_num,
492
- actual_value_text=actual_text,
493
- checked_at=now_iso,
494
- notes=combined_notes,
495
- learning_id=int(learning_id) if learning_id else None,
496
- )
497
- if "error" not in updated:
498
- updated["evaluation"] = status
499
- return updated
500
-
501
-
502
- def set_linked_outcomes_met(
503
- action_type: str,
504
- action_id: str,
505
- *,
506
- metric_source: str = "",
507
- actual_value: float | None = 1.0,
508
- actual_value_text: str = "",
509
- note: str = "",
510
- ) -> list[dict]:
511
- rows = find_pending_outcomes_by_action(action_type, action_id, metric_source=metric_source)
512
- updated: list[dict] = []
513
- for row in rows:
514
- updated.append(
515
- _update_outcome(
516
- int(row["id"]),
517
- status="met",
518
- actual_value=actual_value,
519
- actual_value_text=actual_value_text or row.get("actual_value_text", ""),
520
- checked_at=_utcnow_iso(),
521
- notes=_append_note(row.get("notes"), note or "Linked action reached success state."),
522
- )
523
- )
524
- return updated
525
-
526
-
527
- def list_outcome_pattern_candidates(
528
- *,
529
- min_resolved: int = OUTCOME_PATTERN_MIN_RESOLVED,
530
- min_success_rate: float = OUTCOME_PATTERN_MIN_SUCCESS_RATE,
531
- max_failure_rate: float = OUTCOME_PATTERN_MAX_FAILURE_RATE,
532
- limit: int = 20,
533
- ) -> list[dict]:
534
- conn = get_db()
535
- if not conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='cortex_evaluations'").fetchone():
536
- return []
537
-
538
- rows = conn.execute(
539
- """SELECT
540
- e.area,
541
- e.task_type,
542
- e.goal_profile_id,
543
- e.selected_choice,
544
- SUM(CASE WHEN o.status = 'met' THEN 1 ELSE 0 END) AS met,
545
- SUM(CASE WHEN o.status = 'missed' THEN 1 ELSE 0 END) AS missed,
546
- COUNT(*) AS resolved_outcomes,
547
- MAX(e.created_at) AS last_seen_at
548
- FROM cortex_evaluations e
549
- JOIN outcomes o ON o.id = e.linked_outcome_id
550
- WHERE (e.selected_choice IS NOT NULL AND trim(e.selected_choice) != '')
551
- AND o.status IN ('met', 'missed')
552
- GROUP BY e.area, e.task_type, e.goal_profile_id, e.selected_choice
553
- HAVING COUNT(*) >= ?
554
- ORDER BY resolved_outcomes DESC, last_seen_at DESC
555
- LIMIT ?""",
556
- (max(1, int(min_resolved)), max(1, int(limit * 3))),
557
- ).fetchall()
558
-
559
- candidates: list[dict] = []
560
- for row in rows:
561
- resolved = int(row["resolved_outcomes"] or 0)
562
- met = int(row["met"] or 0)
563
- missed = int(row["missed"] or 0)
564
- if resolved <= 0:
565
- continue
566
- success_rate = round(met / resolved, 3)
567
- candidate_type = ""
568
- if success_rate >= float(min_success_rate):
569
- candidate_type = "reinforce_strategy"
570
- elif success_rate <= float(max_failure_rate):
571
- candidate_type = "avoid_strategy"
572
- if not candidate_type:
573
- continue
574
-
575
- evidence_rows = conn.execute(
576
- """SELECT e.id AS evaluation_id, o.id AS outcome_id, o.status, o.description, e.created_at
577
- FROM cortex_evaluations e
578
- JOIN outcomes o ON o.id = e.linked_outcome_id
579
- WHERE e.area = ?
580
- AND e.task_type = ?
581
- AND e.goal_profile_id = ?
582
- AND e.selected_choice = ?
583
- AND o.status IN ('met', 'missed')
584
- ORDER BY e.created_at DESC, e.id DESC
585
- LIMIT ?""",
586
- (
587
- row["area"] or "",
588
- row["task_type"] or "",
589
- row["goal_profile_id"] or "",
590
- row["selected_choice"] or "",
591
- OUTCOME_PATTERN_MAX_EVIDENCE,
592
- ),
593
- ).fetchall()
594
- evidence = [dict(item) for item in evidence_rows]
595
- context = _context_label(
596
- area=row["area"] or "",
597
- task_type=row["task_type"] or "",
598
- goal_profile_id=row["goal_profile_id"] or "",
599
- )
600
- selected_choice = (row["selected_choice"] or "").strip()
601
- if candidate_type == "reinforce_strategy":
602
- rationale = (
603
- f"La estrategia '{selected_choice}' acumula {met}/{resolved} outcomes met en {context}."
604
- )
605
- else:
606
- rationale = (
607
- f"La estrategia '{selected_choice}' acumula {missed}/{resolved} outcomes missed en {context}."
608
- )
609
-
610
- pattern_key = _pattern_key(
611
- area=row["area"] or "",
612
- task_type=row["task_type"] or "",
613
- goal_profile_id=row["goal_profile_id"] or "",
614
- selected_choice=selected_choice,
615
- )
616
- candidates.append(
617
- {
618
- "pattern_key": pattern_key,
619
- "candidate_type": candidate_type,
620
- "area": row["area"] or "",
621
- "task_type": row["task_type"] or "",
622
- "goal_profile_id": row["goal_profile_id"] or "",
623
- "selected_choice": selected_choice,
624
- "resolved_outcomes": resolved,
625
- "met": met,
626
- "missed": missed,
627
- "success_rate": success_rate,
628
- "last_seen_at": row["last_seen_at"],
629
- "context_label": context,
630
- "rationale": rationale,
631
- "evidence": evidence,
632
- "suggested_skill_candidate": (
633
- candidate_type == "reinforce_strategy" and resolved >= max(4, int(min_resolved))
634
- ),
635
- }
636
- )
637
-
638
- candidates.sort(
639
- key=lambda item: (
640
- 0 if item["candidate_type"] == "avoid_strategy" else 1,
641
- -item["resolved_outcomes"],
642
- item["selected_choice"],
643
- )
644
- )
645
- return candidates[: max(1, int(limit))]
646
-
647
-
648
- def capture_outcome_pattern(
649
- pattern_key: str,
650
- *,
651
- target: str = "learning",
652
- category: str = "outcomes",
653
- ) -> dict:
654
- clean_target = (target or "learning").strip().lower()
655
- if clean_target != "learning":
656
- return {"error": f"Unsupported target: {target}"}
657
-
658
- clean_key = (pattern_key or "").strip()
659
- if not clean_key:
660
- return {"error": "pattern_key is required"}
661
-
662
- candidates = list_outcome_pattern_candidates(limit=200)
663
- candidate = next((item for item in candidates if item["pattern_key"] == clean_key), None)
664
- if not candidate:
665
- return {"error": "Pattern candidate not found or no longer qualifies"}
666
-
667
- selected_choice = candidate["selected_choice"]
668
- context = candidate["context_label"]
669
- applies_to = f"outcome-pattern:{clean_key}"
670
- if candidate["candidate_type"] == "reinforce_strategy":
671
- title = f"Prefer {selected_choice} in {context}"
672
- prevention = (
673
- f"When a comparable context appears, default to '{selected_choice}' unless fresh evidence or constraints override it."
674
- )
675
- else:
676
- title = f"Avoid {selected_choice} in {context}"
677
- prevention = (
678
- f"When a comparable context appears, do not default to '{selected_choice}' until the evidence base changes."
679
- )
680
-
681
- conn = get_db()
682
- existing = conn.execute(
683
- """SELECT * FROM learnings
684
- WHERE status = 'active' AND applies_to = ?
685
- ORDER BY updated_at DESC, id DESC
686
- LIMIT 1""",
687
- (applies_to,),
688
- ).fetchone()
689
- if existing:
690
- return {
691
- "ok": True,
692
- "created": False,
693
- "target": clean_target,
694
- "candidate": candidate,
695
- "learning": dict(existing),
696
- }
697
-
698
- evidence_refs = ", ".join(
699
- f"eval#{item['evaluation_id']}/outcome#{item['outcome_id']}:{item['status']}"
700
- for item in candidate["evidence"][:OUTCOME_PATTERN_MAX_EVIDENCE]
701
- )
702
- content = (
703
- f"{candidate['rationale']} "
704
- f"Success rate: {candidate['success_rate']:.3f}. "
705
- f"Resolved outcomes: {candidate['resolved_outcomes']} "
706
- f"(met={candidate['met']}, missed={candidate['missed']}). "
707
- f"Evidence: {evidence_refs or 'none'}."
708
- )
709
- reasoning = (
710
- "Structured outcome pattern captured from repeated resolved cortex-linked outcomes. "
711
- f"Pattern key: {clean_key}."
712
- )
713
- from db._learnings import create_learning
714
-
715
- learning = create_learning(
716
- category=(category or "outcomes").strip(),
717
- title=title,
718
- content=content,
719
- reasoning=reasoning,
720
- prevention=prevention,
721
- applies_to=applies_to,
722
- )
723
- return {
724
- "ok": True,
725
- "created": True,
726
- "target": clean_target,
727
- "candidate": candidate,
728
- "learning": learning,
729
- }
730
-
731
-
732
- def get_outcome_pattern_learning_signal(
733
- *,
734
- area: str = "",
735
- task_type: str = "",
736
- goal_profile_id: str = "",
737
- selected_choice: str = "",
738
- ) -> dict:
739
- clean_choice = (selected_choice or "").strip()
740
- if not clean_choice:
741
- return {
742
- "active": False,
743
- "pattern_key": "",
744
- "learning_id": 0,
745
- "mode": "",
746
- "title": "",
747
- "success_adjustment": 0.0,
748
- "risk_adjustment": 0.0,
749
- }
750
-
751
- pattern_key = _pattern_key(
752
- area=area or "",
753
- task_type=task_type or "",
754
- goal_profile_id=goal_profile_id or "",
755
- selected_choice=clean_choice,
756
- )
757
- applies_to = f"outcome-pattern:{pattern_key}"
758
- conn = get_db()
759
- row = conn.execute(
760
- """SELECT id, title
761
- FROM learnings
762
- WHERE status = 'active' AND applies_to = ?
763
- ORDER BY updated_at DESC, id DESC
764
- LIMIT 1""",
765
- (applies_to,),
766
- ).fetchone()
767
- if not row:
768
- return {
769
- "active": False,
770
- "pattern_key": pattern_key,
771
- "learning_id": 0,
772
- "mode": "",
773
- "title": "",
774
- "success_adjustment": 0.0,
775
- "risk_adjustment": 0.0,
776
- }
777
-
778
- title = str(row["title"] or "").strip()
779
- if title.startswith("Prefer "):
780
- mode = "prefer"
781
- success_adjustment = OUTCOME_PATTERN_LEARNING_SUCCESS_BOOST
782
- risk_adjustment = -OUTCOME_PATTERN_LEARNING_RISK_REDUCTION
783
- elif title.startswith("Avoid "):
784
- mode = "avoid"
785
- success_adjustment = OUTCOME_PATTERN_LEARNING_SUCCESS_PENALTY
786
- risk_adjustment = OUTCOME_PATTERN_LEARNING_RISK_PENALTY
787
- else:
788
- mode = "observe"
789
- success_adjustment = 0.0
790
- risk_adjustment = 0.0
791
-
792
- return {
793
- "active": mode in {"prefer", "avoid"},
794
- "pattern_key": pattern_key,
795
- "learning_id": int(row["id"] or 0),
796
- "mode": mode,
797
- "title": title,
798
- "success_adjustment": round(success_adjustment, 2),
799
- "risk_adjustment": round(risk_adjustment, 2),
800
- }