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,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
- }