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