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,330 +0,0 @@
1
- from __future__ import annotations
2
- """NEXO DB — Sessions module."""
3
- import time, secrets, string, sqlite3
4
- from datetime import datetime
5
- from db._core import get_db, _gen_id, now_epoch, local_time_str, SESSION_STALE_SECONDS, MESSAGE_TTL_SECONDS, QUESTION_TTL_SECONDS
6
-
7
- # ── Session operations ──────────────────────────────────────────────
8
-
9
- def now_epoch() -> float:
10
- return time.time()
11
-
12
-
13
- def local_time_str() -> str:
14
- from datetime import datetime
15
- return datetime.now().strftime("%H:%M")
16
-
17
-
18
- import re
19
- _SID_EXACT = re.compile(r'^nexo-\d+-\d+$')
20
- _SID_SEARCH = re.compile(r'nexo-\d+-\d+')
21
-
22
- def _validate_sid(sid: str) -> str:
23
- """Validate and sanitize SID. Extracts clean SID if embedded in text."""
24
- if not sid:
25
- raise ValueError("SID cannot be empty")
26
- sid = sid.strip()
27
- # Clean SID — most common case
28
- if _SID_EXACT.match(sid):
29
- return sid
30
- # Extract SID from text like "SID: nexo-1234-5678\nOther stuff..."
31
- match = _SID_SEARCH.search(sid)
32
- if match:
33
- return match.group(0)
34
- raise ValueError(f"Invalid SID format: {sid[:80]}")
35
-
36
-
37
- def register_session(
38
- sid: str,
39
- task: str,
40
- claude_session_id: str = "",
41
- *,
42
- external_session_id: str = "",
43
- session_client: str = "",
44
- ) -> dict:
45
- """Register or re-register a session."""
46
- sid = _validate_sid(sid)
47
- conn = get_db()
48
- now = now_epoch()
49
- linked_session_id = (external_session_id or claude_session_id or "").strip()
50
- conn.execute(
51
- "INSERT OR REPLACE INTO sessions (sid, task, started_epoch, last_update_epoch, local_time, claude_session_id, external_session_id, session_client) "
52
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
53
- (sid, task, now, now, local_time_str(), linked_session_id, linked_session_id, (session_client or "").strip())
54
- )
55
- conn.commit()
56
- return {"sid": sid, "task": task, "external_session_id": linked_session_id, "session_client": (session_client or "").strip()}
57
-
58
-
59
- def update_session(sid: str, task: str | None) -> dict:
60
- """Update session timestamp (and task if provided). Preserves started_epoch.
61
-
62
- Args:
63
- sid: Session ID.
64
- task: New task description, or None to keep current task (keepalive touch).
65
- """
66
- sid = _validate_sid(sid)
67
- conn = get_db()
68
- now = now_epoch()
69
- row = conn.execute("SELECT started_epoch, task FROM sessions WHERE sid = ?", (sid,)).fetchone()
70
- if row:
71
- effective_task = task if task is not None else row["task"]
72
- conn.execute(
73
- "UPDATE sessions SET task = ?, last_update_epoch = ?, local_time = ? WHERE sid = ?",
74
- (effective_task, now, local_time_str(), sid)
75
- )
76
- else:
77
- effective_task = task or "Unknown"
78
- conn.execute(
79
- "INSERT INTO sessions (sid, task, started_epoch, last_update_epoch, local_time) "
80
- "VALUES (?, ?, ?, ?, ?)",
81
- (sid, effective_task, now, now, local_time_str())
82
- )
83
- conn.commit()
84
- return {"sid": sid, "task": effective_task}
85
-
86
-
87
- def complete_session(sid: str):
88
- """Remove session and its tracked files."""
89
- sid = _validate_sid(sid)
90
- conn = get_db()
91
- conn.execute("PRAGMA foreign_keys=ON")
92
- conn.execute("DELETE FROM tracked_files WHERE sid = ?", (sid,))
93
- conn.execute("DELETE FROM sessions WHERE sid = ?", (sid,))
94
- conn.commit()
95
-
96
-
97
- def get_active_sessions() -> list[dict]:
98
- """Get all sessions updated within STALE threshold."""
99
- conn = get_db()
100
- cutoff = now_epoch() - SESSION_STALE_SECONDS
101
- rows = conn.execute(
102
- "SELECT sid, task, started_epoch, last_update_epoch, local_time "
103
- "FROM sessions WHERE last_update_epoch > ?",
104
- (cutoff,)
105
- ).fetchall()
106
- return [dict(r) for r in rows]
107
-
108
-
109
- def clean_stale_sessions() -> int:
110
- """Remove stale sessions. Returns count removed."""
111
- conn = get_db()
112
- cutoff = now_epoch() - SESSION_STALE_SECONDS
113
- stale = conn.execute(
114
- "SELECT sid FROM sessions WHERE last_update_epoch <= ?", (cutoff,)
115
- ).fetchall()
116
- for row in stale:
117
- conn.execute("DELETE FROM tracked_files WHERE sid = ?", (row["sid"],))
118
- result = conn.execute(
119
- "DELETE FROM sessions WHERE last_update_epoch <= ?", (cutoff,)
120
- )
121
- count = result.rowcount
122
- conn.commit()
123
- return count
124
-
125
-
126
- def search_sessions(keyword: str) -> list[dict]:
127
- """Find sessions whose task contains keyword (case-insensitive)."""
128
- conn = get_db()
129
- cutoff = now_epoch() - SESSION_STALE_SECONDS
130
- rows = conn.execute(
131
- "SELECT sid, task, last_update_epoch, local_time FROM sessions "
132
- "WHERE last_update_epoch > ? AND LOWER(task) LIKE ?",
133
- (cutoff, f"%{keyword.lower()}%")
134
- ).fetchall()
135
- return [dict(r) for r in rows]
136
-
137
-
138
- # ── File tracking ───────────────────────────────────────────────────
139
-
140
- def track_files(sid: str, paths: list[str]) -> dict:
141
- """Track files for a session. Returns conflicts if any."""
142
- conn = get_db()
143
- now = now_epoch()
144
- session = conn.execute("SELECT sid FROM sessions WHERE sid = ?", (sid,)).fetchone()
145
- if not session:
146
- return {"error": f"Session {sid} not found. Register first."}
147
-
148
- for path in paths:
149
- conn.execute(
150
- "INSERT OR IGNORE INTO tracked_files (sid, path, tracked_at) VALUES (?, ?, ?)",
151
- (sid, path, now)
152
- )
153
- conn.commit()
154
- conflicts = _check_conflicts(conn, sid)
155
- return {"tracked": paths, "conflicts": conflicts}
156
-
157
-
158
- def untrack_files(sid: str, paths: list[str] | None = None):
159
- """Untrack files. If paths is None, untrack all."""
160
- conn = get_db()
161
- if paths:
162
- for path in paths:
163
- conn.execute(
164
- "DELETE FROM tracked_files WHERE sid = ? AND path = ?",
165
- (sid, path)
166
- )
167
- else:
168
- conn.execute("DELETE FROM tracked_files WHERE sid = ?", (sid,))
169
- conn.commit()
170
-
171
-
172
- def get_all_tracked_files() -> dict:
173
- """Get all tracked files grouped by session."""
174
- conn = get_db()
175
- cutoff = now_epoch() - SESSION_STALE_SECONDS
176
- rows = conn.execute(
177
- "SELECT tf.sid, tf.path, s.task FROM tracked_files tf "
178
- "JOIN sessions s ON tf.sid = s.sid "
179
- "WHERE s.last_update_epoch > ?",
180
- (cutoff,)
181
- ).fetchall()
182
- result = {}
183
- for r in rows:
184
- sid = r["sid"]
185
- if sid not in result:
186
- result[sid] = {"task": r["task"], "files": []}
187
- result[sid]["files"].append(r["path"])
188
- return result
189
-
190
-
191
- def _check_conflicts(conn: sqlite3.Connection, sid: str) -> list[dict]:
192
- """Check if any of sid's files are tracked by other active sessions."""
193
- cutoff = now_epoch() - SESSION_STALE_SECONDS
194
- my_files = conn.execute(
195
- "SELECT path FROM tracked_files WHERE sid = ?", (sid,)
196
- ).fetchall()
197
- my_paths = {r["path"] for r in my_files}
198
- if not my_paths:
199
- return []
200
-
201
- conflicts = []
202
- others = conn.execute(
203
- "SELECT tf.sid, tf.path, s.task FROM tracked_files tf "
204
- "JOIN sessions s ON tf.sid = s.sid "
205
- "WHERE tf.sid != ? AND s.last_update_epoch > ?",
206
- (sid, cutoff)
207
- ).fetchall()
208
- by_sid = {}
209
- for r in others:
210
- if r["path"] in my_paths:
211
- osid = r["sid"]
212
- if osid not in by_sid:
213
- by_sid[osid] = {"sid": osid, "task": r["task"], "files": []}
214
- by_sid[osid]["files"].append(r["path"])
215
- return list(by_sid.values())
216
-
217
-
218
- # ── Messages ────────────────────────────────────────────────────────
219
-
220
- def send_message(from_sid: str, to_sid: str, text: str) -> str:
221
- """Send a message. to_sid can be 'all' for broadcast."""
222
- conn = get_db()
223
- _clean_old_messages(conn)
224
- msg_id = _gen_id("msg", 6)
225
- conn.execute(
226
- "INSERT INTO messages (id, from_sid, to_sid, text, created_epoch) "
227
- "VALUES (?, ?, ?, ?, ?)",
228
- (msg_id, from_sid, to_sid, text, now_epoch())
229
- )
230
- conn.commit()
231
- return msg_id
232
-
233
-
234
- def get_inbox(sid: str) -> list[dict]:
235
- """Get unread messages for a session."""
236
- conn = get_db()
237
- _clean_old_messages(conn)
238
- rows = conn.execute(
239
- "SELECT m.id, m.from_sid, m.to_sid, m.text, m.created_epoch "
240
- "FROM messages m "
241
- "WHERE (m.to_sid = 'all' OR m.to_sid = ?) "
242
- "AND m.from_sid != ? "
243
- "AND m.id NOT IN (SELECT message_id FROM message_reads WHERE sid = ?)",
244
- (sid, sid, sid)
245
- ).fetchall()
246
- for r in rows:
247
- conn.execute(
248
- "INSERT OR IGNORE INTO message_reads (message_id, sid) VALUES (?, ?)",
249
- (r["id"], sid)
250
- )
251
- conn.commit()
252
- result = [dict(r) for r in rows]
253
- return result
254
-
255
-
256
- def _clean_old_messages(conn: sqlite3.Connection):
257
- """Remove expired messages and commit immediately."""
258
- cutoff = now_epoch() - MESSAGE_TTL_SECONDS
259
- conn.execute("DELETE FROM messages WHERE created_epoch < ?", (cutoff,))
260
- conn.commit()
261
-
262
-
263
- # ── Questions ───────────────────────────────────────────────────────
264
-
265
- def ask_question(from_sid: str, to_sid: str, question: str) -> str:
266
- """Create a pending question. Returns qid."""
267
- conn = get_db()
268
- _expire_old_questions(conn)
269
- qid = _gen_id("q", 8)
270
- conn.execute(
271
- "INSERT INTO questions (qid, from_sid, to_sid, question, status, created_epoch) "
272
- "VALUES (?, ?, ?, ?, 'pending', ?)",
273
- (qid, from_sid, to_sid, question, now_epoch())
274
- )
275
- conn.commit()
276
- return qid
277
-
278
-
279
- def answer_question(qid: str, answer: str) -> dict:
280
- """Answer a pending question."""
281
- conn = get_db()
282
- row = conn.execute(
283
- "SELECT * FROM questions WHERE qid = ?", (qid,)
284
- ).fetchone()
285
- if not row:
286
- return {"error": f"Question {qid} not found"}
287
- if row["status"] != "pending":
288
- return {"error": f"Question {qid} is {row['status']}, not pending"}
289
- conn.execute(
290
- "UPDATE questions SET answer = ?, status = 'answered', answered_epoch = ? "
291
- "WHERE qid = ?",
292
- (answer, now_epoch(), qid)
293
- )
294
- conn.commit()
295
- return {"qid": qid, "status": "answered"}
296
-
297
-
298
- def get_pending_questions(sid: str) -> list[dict]:
299
- """Get pending questions addressed to this session."""
300
- conn = get_db()
301
- _expire_old_questions(conn)
302
- rows = conn.execute(
303
- "SELECT qid, from_sid, question, created_epoch FROM questions "
304
- "WHERE to_sid = ? AND status = 'pending'",
305
- (sid,)
306
- ).fetchall()
307
- conn.commit()
308
- return [dict(r) for r in rows]
309
-
310
-
311
- def check_answer(qid: str) -> dict | None:
312
- """Check if a question has been answered. Returns answer or None."""
313
- conn = get_db()
314
- row = conn.execute(
315
- "SELECT qid, answer, status FROM questions WHERE qid = ?", (qid,)
316
- ).fetchone()
317
- if not row:
318
- return None
319
- return dict(row)
320
-
321
-
322
- def _expire_old_questions(conn: sqlite3.Connection):
323
- """Mark old pending questions as expired."""
324
- cutoff = now_epoch() - QUESTION_TTL_SECONDS
325
- conn.execute(
326
- "UPDATE questions SET status = 'expired' "
327
- "WHERE status = 'pending' AND created_epoch < ?",
328
- (cutoff,)
329
- )
330
-
@@ -1,91 +0,0 @@
1
- """NEXO DB — Tasks module."""
2
- from db._core import get_db, now_epoch
3
-
4
- # ── Task History & Frequencies ─────────────────────────────────────
5
-
6
- def log_task(task_num: str, task_name: str, notes: str = '', reasoning: str = '') -> dict:
7
- """Log a task execution with optional reasoning."""
8
- conn = get_db()
9
- now = now_epoch()
10
- cursor = conn.execute(
11
- "INSERT INTO task_history (task_num, task_name, executed_at, notes, reasoning) "
12
- "VALUES (?, ?, ?, ?, ?)",
13
- (task_num, task_name, now, notes, reasoning)
14
- )
15
- conn.commit()
16
- row = conn.execute(
17
- "SELECT * FROM task_history WHERE id = ?", (cursor.lastrowid,)
18
- ).fetchone()
19
- return dict(row)
20
-
21
-
22
- def list_task_history(task_num: str = None, days: int = 30) -> list[dict]:
23
- """List task execution history, optionally filtered by task_num."""
24
- conn = get_db()
25
- cutoff = now_epoch() - (days * 86400)
26
- if task_num:
27
- rows = conn.execute(
28
- "SELECT * FROM task_history WHERE task_num = ? AND executed_at >= ? "
29
- "ORDER BY executed_at DESC",
30
- (task_num, cutoff)
31
- ).fetchall()
32
- else:
33
- rows = conn.execute(
34
- "SELECT * FROM task_history WHERE executed_at >= ? "
35
- "ORDER BY executed_at DESC",
36
- (cutoff,)
37
- ).fetchall()
38
- return [dict(r) for r in rows]
39
-
40
-
41
- def set_task_frequency(task_num: str, task_name: str,
42
- frequency_days: int, description: str = '') -> dict:
43
- """Set or update the expected frequency for a task."""
44
- conn = get_db()
45
- conn.execute(
46
- "INSERT OR REPLACE INTO task_frequencies (task_num, task_name, frequency_days, description) "
47
- "VALUES (?, ?, ?, ?)",
48
- (task_num, task_name, frequency_days, description)
49
- )
50
- conn.commit()
51
- row = conn.execute(
52
- "SELECT * FROM task_frequencies WHERE task_num = ?", (task_num,)
53
- ).fetchone()
54
- return dict(row)
55
-
56
-
57
- def get_overdue_tasks() -> list[dict]:
58
- """Get tasks where last execution exceeds the configured frequency."""
59
- conn = get_db()
60
- freqs = conn.execute("SELECT * FROM task_frequencies").fetchall()
61
- now = now_epoch()
62
- overdue = []
63
- for f in freqs:
64
- last = conn.execute(
65
- "SELECT MAX(executed_at) as last_exec FROM task_history WHERE task_num = ?",
66
- (f["task_num"],)
67
- ).fetchone()
68
- last_exec = last["last_exec"] if last and last["last_exec"] else None
69
- threshold = f["frequency_days"] * 86400
70
- if last_exec is None or (now - last_exec) > threshold:
71
- days_ago = round((now - last_exec) / 86400, 1) if last_exec else None
72
- overdue.append({
73
- "task_num": f["task_num"],
74
- "task_name": f["task_name"],
75
- "frequency_days": f["frequency_days"],
76
- "last_executed": last_exec,
77
- "days_since_last": days_ago,
78
- "description": f["description"]
79
- })
80
- return overdue
81
-
82
-
83
- def get_task_frequencies() -> list[dict]:
84
- """Get all configured task frequencies."""
85
- conn = get_db()
86
- rows = conn.execute(
87
- "SELECT * FROM task_frequencies ORDER BY task_num ASC"
88
- ).fetchall()
89
- return [dict(r) for r in rows]
90
-
91
-
@@ -1,173 +0,0 @@
1
- from __future__ import annotations
2
- """NEXO DB — state watchers registry."""
3
-
4
- import json
5
- import secrets
6
- import time
7
-
8
- from db._core import get_db
9
-
10
- WATCHER_TYPES = {"repo_drift", "cron_drift", "api_health", "environment_drift", "expiry"}
11
- WATCHER_STATUSES = {"active", "paused", "archived"}
12
- WATCHER_HEALTH = {"unknown", "healthy", "degraded", "critical"}
13
-
14
-
15
- def _watcher_id() -> str:
16
- return f"SW-{int(time.time())}-{secrets.randbelow(100000)}"
17
-
18
-
19
- def _as_json(value, default):
20
- if value is None:
21
- value = default
22
- if isinstance(value, str):
23
- return value
24
- return json.dumps(value, ensure_ascii=False)
25
-
26
-
27
- def _parse_json(value, default):
28
- if value in (None, ""):
29
- return default
30
- if isinstance(value, (dict, list)):
31
- return value
32
- try:
33
- return json.loads(value)
34
- except Exception:
35
- return default
36
-
37
-
38
- def _row_to_watcher(row) -> dict:
39
- watcher = dict(row)
40
- watcher["config"] = _parse_json(watcher.get("config"), {})
41
- watcher["last_result"] = _parse_json(watcher.get("last_result"), {})
42
- return watcher
43
-
44
-
45
- def create_state_watcher(
46
- watcher_type: str,
47
- title: str,
48
- *,
49
- target: str = "",
50
- severity: str = "warn",
51
- status: str = "active",
52
- config=None,
53
- ) -> dict:
54
- clean_type = str(watcher_type or "").strip().lower()
55
- if clean_type not in WATCHER_TYPES:
56
- raise ValueError(f"Unsupported watcher_type: {watcher_type}")
57
- clean_status = str(status or "active").strip().lower()
58
- if clean_status not in WATCHER_STATUSES:
59
- clean_status = "active"
60
- watcher_id = _watcher_id()
61
- conn = get_db()
62
- conn.execute(
63
- """INSERT INTO state_watchers (
64
- watcher_id, watcher_type, title, target, severity, status, config,
65
- last_health, last_result, last_checked_at
66
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
67
- (
68
- watcher_id,
69
- clean_type,
70
- str(title or "").strip(),
71
- str(target or "").strip(),
72
- str(severity or "warn").strip().lower() or "warn",
73
- clean_status,
74
- _as_json(config, {}),
75
- "unknown",
76
- "{}",
77
- "",
78
- ),
79
- )
80
- conn.commit()
81
- return get_state_watcher(watcher_id) or {"watcher_id": watcher_id}
82
-
83
-
84
- def get_state_watcher(watcher_id: str) -> dict | None:
85
- conn = get_db()
86
- row = conn.execute(
87
- "SELECT * FROM state_watchers WHERE watcher_id = ?",
88
- (str(watcher_id or "").strip(),),
89
- ).fetchone()
90
- return _row_to_watcher(row) if row else None
91
-
92
-
93
- def list_state_watchers(*, status: str = "", watcher_type: str = "", limit: int = 100) -> list[dict]:
94
- clauses: list[str] = []
95
- params: list[object] = []
96
- if status:
97
- clauses.append("status = ?")
98
- params.append(str(status).strip().lower())
99
- if watcher_type:
100
- clauses.append("watcher_type = ?")
101
- params.append(str(watcher_type).strip().lower())
102
- where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
103
- params.append(max(1, int(limit or 100)))
104
- conn = get_db()
105
- rows = conn.execute(
106
- f"""SELECT *
107
- FROM state_watchers
108
- {where}
109
- ORDER BY updated_at DESC, watcher_id DESC
110
- LIMIT ?""",
111
- tuple(params),
112
- ).fetchall()
113
- return [_row_to_watcher(row) for row in rows]
114
-
115
-
116
- def update_state_watcher(
117
- watcher_id: str,
118
- *,
119
- title: str | None = None,
120
- target: str | None = None,
121
- severity: str | None = None,
122
- status: str | None = None,
123
- config=None,
124
- ) -> dict | None:
125
- current = get_state_watcher(watcher_id)
126
- if not current:
127
- return None
128
- updates = {
129
- "title": current["title"] if title is None else str(title).strip(),
130
- "target": current["target"] if target is None else str(target).strip(),
131
- "severity": current["severity"] if severity is None else str(severity).strip().lower(),
132
- "status": current["status"] if status is None else str(status).strip().lower(),
133
- "config": _as_json(current["config"] if config is None else config, {}),
134
- }
135
- if updates["status"] not in WATCHER_STATUSES:
136
- updates["status"] = current["status"]
137
- conn = get_db()
138
- conn.execute(
139
- """UPDATE state_watchers
140
- SET title = ?, target = ?, severity = ?, status = ?, config = ?,
141
- updated_at = datetime('now')
142
- WHERE watcher_id = ?""",
143
- (
144
- updates["title"],
145
- updates["target"],
146
- updates["severity"],
147
- updates["status"],
148
- updates["config"],
149
- str(watcher_id).strip(),
150
- ),
151
- )
152
- conn.commit()
153
- return get_state_watcher(watcher_id)
154
-
155
-
156
- def update_state_watcher_result(watcher_id: str, *, health: str, result=None, checked_at: str = "") -> dict | None:
157
- clean_health = str(health or "unknown").strip().lower()
158
- if clean_health not in WATCHER_HEALTH:
159
- clean_health = "unknown"
160
- conn = get_db()
161
- conn.execute(
162
- """UPDATE state_watchers
163
- SET last_health = ?, last_result = ?, last_checked_at = ?, updated_at = datetime('now')
164
- WHERE watcher_id = ?""",
165
- (
166
- clean_health,
167
- _as_json(result, {}),
168
- checked_at.strip(),
169
- str(watcher_id or "").strip(),
170
- ),
171
- )
172
- conn.commit()
173
- return get_state_watcher(watcher_id)
@@ -1,52 +0,0 @@
1
- """Doctor output formatters — text and JSON."""
2
- from __future__ import annotations
3
-
4
- import json
5
- from dataclasses import asdict
6
-
7
- from doctor.models import DoctorReport
8
-
9
-
10
- def format_report(report: DoctorReport, fmt: str = "text") -> str:
11
- """Format a DoctorReport as text or JSON."""
12
- if fmt == "json":
13
- return json.dumps(asdict(report), indent=2, ensure_ascii=False)
14
- return _format_text(report)
15
-
16
-
17
- def _format_text(report: DoctorReport) -> str:
18
- """Human-friendly text output."""
19
- lines = []
20
-
21
- # Header
22
- icon = {"healthy": "✓", "degraded": "⚠", "critical": "✗"}.get(report.overall_status, "?")
23
- lines.append(f"NEXO Doctor — {icon} {report.overall_status.upper()}")
24
- lines.append(f" {report.counts.get('healthy', 0)} healthy, "
25
- f"{report.counts.get('degraded', 0)} degraded, "
26
- f"{report.counts.get('critical', 0)} critical "
27
- f"({report.duration_ms}ms)")
28
- lines.append("")
29
-
30
- # Group by tier
31
- current_tier = None
32
- for check in report.checks:
33
- if check.tier != current_tier:
34
- current_tier = check.tier
35
- lines.append(f"── {current_tier.upper()} ──")
36
-
37
- icon = {"healthy": "✓", "degraded": "⚠", "critical": "✗"}.get(check.status, "?")
38
- fixed = " [FIXED]" if check.fixed else ""
39
- lines.append(f" {icon} {check.summary}{fixed}")
40
-
41
- if check.status != "healthy":
42
- for ev in check.evidence:
43
- lines.append(f" → {ev}")
44
- if check.repair_plan:
45
- lines.append(" Fix:")
46
- for step in check.repair_plan:
47
- lines.append(f" • {step}")
48
- if check.escalation_prompt:
49
- lines.append(f" Escalation: {check.escalation_prompt}")
50
-
51
- lines.append("")
52
- return "\n".join(lines)