nexo-brain 2.2.0 → 2.3.1

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 (256) hide show
  1. package/README.md +5 -5
  2. package/package.json +6 -3
  3. package/src/auto_update.py +26 -0
  4. package/src/crons/manifest.json +6 -13
  5. package/src/crons/sync.py +150 -6
  6. package/src/db/__init__.py +13 -0
  7. package/src/db/_core.py +1 -0
  8. package/src/db/_cron_runs.py +74 -0
  9. package/src/db/_entities.py +1 -0
  10. package/src/db/_episodic.py +41 -6
  11. package/src/db/_learnings.py +1 -0
  12. package/src/db/_reminders.py +1 -0
  13. package/src/db/_schema.py +64 -0
  14. package/src/db/_sessions.py +1 -0
  15. package/src/db/_skills.py +515 -0
  16. package/src/hooks/session-stop.sh +13 -101
  17. package/src/plugin_loader.py +1 -0
  18. package/src/plugins/episodic_memory.py +5 -3
  19. package/src/plugins/schedule.py +212 -0
  20. package/src/plugins/skills.py +264 -0
  21. package/src/plugins/update.py +1 -0
  22. package/src/scripts/deep-sleep/apply_findings.py +111 -8
  23. package/src/scripts/deep-sleep/collect.py +34 -11
  24. package/src/scripts/deep-sleep/extract-prompt.md +38 -0
  25. package/src/scripts/deep-sleep/extract.py +81 -8
  26. package/src/scripts/deep-sleep/synthesize-prompt.md +29 -1
  27. package/src/scripts/deep-sleep/synthesize.py +4 -1
  28. package/src/scripts/nexo-catchup.py +65 -29
  29. package/src/scripts/nexo-cron-wrapper.sh +53 -0
  30. package/src/scripts/nexo-daily-self-audit.py +4 -2
  31. package/src/scripts/nexo-deep-sleep.sh +66 -77
  32. package/src/scripts/nexo-evolution-run.py +13 -0
  33. package/src/scripts/nexo-learning-housekeep.py +157 -1
  34. package/src/scripts/nexo-learning-validator.py +19 -0
  35. package/src/scripts/nexo-postmortem-consolidator.py +3 -2
  36. package/src/scripts/nexo-sleep.py +16 -11
  37. package/src/scripts/nexo-synthesis.py +46 -3
  38. package/src/scripts/nexo-watchdog.sh +91 -30
  39. package/src/server.py +6 -1
  40. package/src/tools_coordination.py +1 -0
  41. package/src/tools_sessions.py +1 -0
  42. package/scripts/migrate-to-unified 2.sh +0 -813
  43. package/scripts/migrate-to-unified.sh +0 -813
  44. package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
  45. package/scripts/migrate-v1.5-to-v1.6.py +0 -778
  46. package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
  47. package/scripts/migrate-v1.7-to-v1.8.py +0 -214
  48. package/scripts/pre-commit-check 2.sh +0 -55
  49. package/scripts/pre-commit-check.sh +0 -55
  50. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  51. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  52. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  53. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  54. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  55. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  56. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  57. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  58. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  59. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  60. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  61. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  62. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  63. package/src/auto_close_sessions 2.py +0 -159
  64. package/src/auto_update 2.py +0 -634
  65. package/src/claim_graph 2.py +0 -323
  66. package/src/cognitive/__init__ 2.py +0 -62
  67. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  68. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  69. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  70. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  71. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  72. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  73. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  74. package/src/cognitive/_core 2.py +0 -567
  75. package/src/cognitive/_decay 2.py +0 -382
  76. package/src/cognitive/_ingest 2.py +0 -892
  77. package/src/cognitive/_memory 2.py +0 -912
  78. package/src/cognitive/_search 2.py +0 -949
  79. package/src/cognitive/_trust 2.py +0 -464
  80. package/src/crons/manifest 2.json +0 -106
  81. package/src/crons/sync 2.py +0 -217
  82. package/src/dashboard/__init__ 2.py +0 -0
  83. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  84. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  85. package/src/dashboard/app 2.py +0 -789
  86. package/src/db/__init__ 2.py +0 -89
  87. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  90. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  91. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  92. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  93. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  94. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  95. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  96. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  97. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  98. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  99. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  110. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  111. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  112. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  113. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  114. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  115. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  116. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  117. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  118. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  119. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  120. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  121. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  122. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  123. package/src/db/_core 2.py +0 -417
  124. package/src/db/_credentials 2.py +0 -124
  125. package/src/db/_entities 2.py +0 -178
  126. package/src/db/_episodic 2.py +0 -738
  127. package/src/db/_evolution 2.py +0 -54
  128. package/src/db/_fts 2.py +0 -406
  129. package/src/db/_learnings 2.py +0 -168
  130. package/src/db/_reminders 2.py +0 -338
  131. package/src/db/_schema 2.py +0 -364
  132. package/src/db/_sessions 2.py +0 -300
  133. package/src/db/_tasks 2.py +0 -91
  134. package/src/evolution_cycle 2.py +0 -266
  135. package/src/hnsw_index 2.py +0 -254
  136. package/src/hooks/auto_capture 2.py +0 -208
  137. package/src/hooks/caffeinate-guard 2.sh +0 -8
  138. package/src/hooks/capture-session 2.sh +0 -21
  139. package/src/hooks/capture-tool-logs 2.sh +0 -127
  140. package/src/hooks/daily-briefing-check 2.sh +0 -33
  141. package/src/hooks/inbox-hook 2.sh +0 -76
  142. package/src/hooks/post-compact 2.sh +0 -148
  143. package/src/hooks/pre-compact 2.sh +0 -151
  144. package/src/hooks/session-start 2.sh +0 -268
  145. package/src/hooks/session-stop 2.sh +0 -140
  146. package/src/kg_populate 2.py +0 -290
  147. package/src/knowledge_graph 2.py +0 -257
  148. package/src/maintenance 2.py +0 -59
  149. package/src/migrate_embeddings 2.py +0 -122
  150. package/src/plugin_loader 2.py +0 -202
  151. package/src/plugins/__init__ 2.py +0 -0
  152. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  153. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  154. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  155. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  156. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  157. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  158. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  159. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  160. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  161. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  162. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  163. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  164. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  165. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  166. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  167. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  168. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  169. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  172. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  175. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  182. package/src/plugins/adaptive_mode 2.py +0 -805
  183. package/src/plugins/agents 2.py +0 -52
  184. package/src/plugins/artifact_registry 2.py +0 -450
  185. package/src/plugins/backup 2.py +0 -104
  186. package/src/plugins/cognitive_memory 2.py +0 -564
  187. package/src/plugins/core_rules 2.py +0 -252
  188. package/src/plugins/cortex 2.py +0 -299
  189. package/src/plugins/entities 2.py +0 -67
  190. package/src/plugins/episodic_memory 2.py +0 -533
  191. package/src/plugins/evolution 2.py +0 -115
  192. package/src/plugins/guard 2.py +0 -746
  193. package/src/plugins/knowledge_graph_tools 2.py +0 -105
  194. package/src/plugins/preferences 2.py +0 -47
  195. package/src/plugins/update 2.py +0 -256
  196. package/src/requirements 2.txt +0 -12
  197. package/src/rules/__init__ 2.py +0 -0
  198. package/src/rules/core-rules 2.json +0 -331
  199. package/src/rules/migrate 2.py +0 -207
  200. package/src/scripts/check-context 2.py +0 -264
  201. package/src/scripts/nexo-auto-update 2.py +0 -6
  202. package/src/scripts/nexo-backup 2.sh +0 -25
  203. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  204. package/src/scripts/nexo-catchup 2.py +0 -242
  205. package/src/scripts/nexo-cognitive-decay 2.py +0 -182
  206. package/src/scripts/nexo-daily-self-audit 2.py +0 -552
  207. package/src/scripts/nexo-deep-sleep 2.sh +0 -97
  208. package/src/scripts/nexo-evolution-run 2.py +0 -597
  209. package/src/scripts/nexo-followup-hygiene 2.py +0 -112
  210. package/src/scripts/nexo-github-monitor 2.py +0 -256
  211. package/src/scripts/nexo-github-monitor.py +0 -256
  212. package/src/scripts/nexo-immune 2.py +0 -927
  213. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  214. package/src/scripts/nexo-install 2.py +0 -6
  215. package/src/scripts/nexo-learning-housekeep 2.py +0 -245
  216. package/src/scripts/nexo-learning-validator 2.py +0 -207
  217. package/src/scripts/nexo-migrate 2.py +0 -232
  218. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
  219. package/src/scripts/nexo-pre-commit 2.py +0 -120
  220. package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
  221. package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
  222. package/src/scripts/nexo-reflection 2.py +0 -253
  223. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  224. package/src/scripts/nexo-send-email 2.py +0 -25
  225. package/src/scripts/nexo-send-email.py +0 -25
  226. package/src/scripts/nexo-send-reply 2.py +0 -178
  227. package/src/scripts/nexo-send-reply.py +0 -178
  228. package/src/scripts/nexo-sleep 2.py +0 -592
  229. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  230. package/src/scripts/nexo-synthesis 2.py +0 -253
  231. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  232. package/src/scripts/nexo-update 2.sh +0 -161
  233. package/src/scripts/nexo-watchdog 2.sh +0 -878
  234. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  235. package/src/server 2.py +0 -733
  236. package/src/storage_router 2.py +0 -32
  237. package/src/tools_coordination 2.py +0 -102
  238. package/src/tools_credentials 2.py +0 -68
  239. package/src/tools_learnings 2.py +0 -220
  240. package/src/tools_menu 2.py +0 -227
  241. package/src/tools_reminders 2.py +0 -86
  242. package/src/tools_reminders_crud 2.py +0 -159
  243. package/src/tools_sessions 2.py +0 -476
  244. package/src/tools_task_history 2.py +0 -57
  245. package/templates/CLAUDE.md 2.template +0 -63
  246. package/templates/openclaw 2.json +0 -13
  247. package/tests/__init__ 2.py +0 -0
  248. package/tests/__init__.py +0 -0
  249. package/tests/conftest 2.py +0 -71
  250. package/tests/conftest.py +0 -71
  251. package/tests/test_cognitive 2.py +0 -205
  252. package/tests/test_cognitive.py +0 -205
  253. package/tests/test_knowledge_graph 2.py +0 -140
  254. package/tests/test_knowledge_graph.py +0 -140
  255. package/tests/test_migrations 2.py +0 -137
  256. package/tests/test_migrations.py +0 -137
@@ -1,382 +0,0 @@
1
- """NEXO Cognitive — Decay, promotion, garbage collection, dream consolidation."""
2
- import math
3
- import numpy as np
4
- from datetime import datetime, timedelta
5
- from cognitive._core import _get_db, embed, cosine_similarity, _blob_to_array, _array_to_blob, LAMBDA_STM, LAMBDA_LTM, EMBEDDING_DIM
6
-
7
-
8
- def _hnsw_invalidate():
9
- """Invalidate HNSW indices after bulk operations (best-effort)."""
10
- try:
11
- import hnsw_index
12
- if hnsw_index.is_available():
13
- hnsw_index.invalidate("both")
14
- except Exception:
15
- pass
16
-
17
-
18
- def apply_decay(adaptive: bool = True):
19
- """Apply Ebbinghaus decay to all memories. Mark LTM as dormant if strength < 0.1.
20
-
21
- Args:
22
- adaptive: If True, protect unique memories (no similar neighbors) from aggressive decay.
23
- Unique memories decay at 25% of normal rate. This prevents information loss
24
- in sparse memory stores where there's no redundancy to compensate.
25
- """
26
- db = _get_db()
27
- now = datetime.utcnow()
28
-
29
- # Build redundancy map if adaptive mode — check which memories have similar siblings
30
- _protected_stm = set()
31
- _protected_ltm = set()
32
- if adaptive:
33
- # A memory is "protected" if it has no siblings in memory_siblings table
34
- # (meaning no other memory covers similar content)
35
- sibling_ids = set()
36
- for row in db.execute("SELECT memory_a_id, memory_b_id FROM memory_siblings").fetchall():
37
- sibling_ids.add(row["memory_a_id"])
38
- sibling_ids.add(row["memory_b_id"])
39
-
40
- # STM memories NOT in sibling_ids are unique → protect
41
- for row in db.execute("SELECT id FROM stm_memories WHERE promoted_to_ltm = 0").fetchall():
42
- if row["id"] not in sibling_ids:
43
- _protected_stm.add(row["id"])
44
-
45
- # LTM memories NOT in sibling_ids are unique → protect
46
- for row in db.execute("SELECT id FROM ltm_memories WHERE is_dormant = 0").fetchall():
47
- if row["id"] not in sibling_ids:
48
- _protected_ltm.add(row["id"])
49
-
50
- # STM decay (skip pinned)
51
- rows = db.execute("SELECT id, last_accessed, strength FROM stm_memories WHERE promoted_to_ltm = 0 AND (lifecycle_state IS NULL OR lifecycle_state != 'pinned')").fetchall()
52
- for row in rows:
53
- last = datetime.fromisoformat(row["last_accessed"])
54
- hours = (now - last).total_seconds() / 3600.0
55
- decay_rate = LAMBDA_STM * 0.25 if (adaptive and row["id"] in _protected_stm) else LAMBDA_STM
56
- new_strength = row["strength"] * math.exp(-decay_rate * hours)
57
- db.execute("UPDATE stm_memories SET strength = ? WHERE id = ?", (new_strength, row["id"]))
58
-
59
- # LTM decay (skip pinned)
60
- rows = db.execute("SELECT id, last_accessed, strength FROM ltm_memories WHERE is_dormant = 0 AND (lifecycle_state IS NULL OR lifecycle_state != 'pinned')").fetchall()
61
- for row in rows:
62
- last = datetime.fromisoformat(row["last_accessed"])
63
- hours = (now - last).total_seconds() / 3600.0
64
- decay_rate = LAMBDA_LTM * 0.25 if (adaptive and row["id"] in _protected_ltm) else LAMBDA_LTM
65
- new_strength = row["strength"] * math.exp(-decay_rate * hours)
66
- if new_strength < 0.1:
67
- db.execute("UPDATE ltm_memories SET strength = ?, is_dormant = 1 WHERE id = ?", (new_strength, row["id"]))
68
- else:
69
- db.execute("UPDATE ltm_memories SET strength = ? WHERE id = ?", (new_strength, row["id"]))
70
-
71
- db.commit()
72
-
73
-
74
- def promote_stm_to_ltm():
75
- """Promote STM memories to LTM based on multiple criteria.
76
-
77
- Promotion rules (any one is sufficient):
78
- 1. access_count >= 3 (actively retrieved = important)
79
- 2. age > 5 days AND strength > 0.4 (survived decay = worth keeping)
80
- 3. source_type in ('learning', 'decision', 'feedback') (high-value by nature)
81
- """
82
- db = _get_db()
83
- now = datetime.utcnow()
84
- age_cutoff = (now - timedelta(days=5)).isoformat()
85
-
86
- # Rule 1: frequently accessed
87
- # Rule 2: old + strong (survived decay)
88
- # Rule 3: high-value source types (always promote if in STM)
89
- rows = db.execute(
90
- """SELECT * FROM stm_memories
91
- WHERE promoted_to_ltm = 0
92
- AND (
93
- access_count >= 3
94
- OR (created_at < ? AND strength > 0.4)
95
- OR source_type IN ('learning', 'decision', 'feedback')
96
- )""",
97
- (age_cutoff,)
98
- ).fetchall()
99
-
100
- promoted = 0
101
- for row in rows:
102
- redacted = row["redaction_applied"] if "redaction_applied" in row.keys() else 0
103
- db.execute(
104
- """INSERT INTO ltm_memories (content, embedding, source_type, source_id, source_title, domain, original_stm_id, redaction_applied)
105
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
106
- (row["content"], row["embedding"], row["source_type"], row["source_id"],
107
- row["source_title"], row["domain"], row["id"], redacted)
108
- )
109
- db.execute("UPDATE stm_memories SET promoted_to_ltm = 1 WHERE id = ?", (row["id"],))
110
- promoted += 1
111
-
112
- db.commit()
113
- if promoted > 0:
114
- _hnsw_invalidate()
115
- return promoted
116
-
117
-
118
- def gc_stm():
119
- """Garbage collect STM: delete weak old memories and anything > 45 days.
120
- Pinned memories are never deleted.
121
- """
122
- db = _get_db()
123
- now = datetime.utcnow()
124
- cutoff_7d = (now - timedelta(days=7)).isoformat()
125
- cutoff_45d = (now - timedelta(days=45)).isoformat()
126
-
127
- # Delete STM with strength < 0.3 and older than 7 days (skip pinned)
128
- cur1 = db.execute(
129
- "DELETE FROM stm_memories WHERE strength < 0.3 AND created_at < ? AND promoted_to_ltm = 0 "
130
- "AND (lifecycle_state IS NULL OR lifecycle_state != 'pinned')",
131
- (cutoff_7d,)
132
- )
133
- # Delete any STM older than 45 days (skip pinned)
134
- cur2 = db.execute(
135
- "DELETE FROM stm_memories WHERE created_at < ? AND promoted_to_ltm = 0 "
136
- "AND (lifecycle_state IS NULL OR lifecycle_state != 'pinned')",
137
- (cutoff_45d,)
138
- )
139
- db.commit()
140
- deleted = (cur1.rowcount or 0) + (cur2.rowcount or 0)
141
- if deleted > 0:
142
- _hnsw_invalidate()
143
- return deleted
144
-
145
-
146
- def gc_test_memories() -> int:
147
- """Purge STM memories from test/dev sessions that pollute strength metrics.
148
- Removes memories with test domains or known test content patterns.
149
- Returns count of deleted memories.
150
- """
151
- db = _get_db()
152
- test_domains = ("test", "test_session")
153
- deleted = 0
154
-
155
- # 1. Delete by test domain
156
- for domain in test_domains:
157
- cur = db.execute(
158
- "DELETE FROM stm_memories WHERE domain = ? "
159
- "AND (lifecycle_state IS NULL OR lifecycle_state != 'pinned')",
160
- (domain,)
161
- )
162
- deleted += cur.rowcount or 0
163
-
164
- # 2. Delete known test content patterns (empty domain, test-like content)
165
- test_patterns = [
166
- "%Secret redact test%",
167
- "%quarantine test fact%",
168
- "%Pin test memory%",
169
- "%API rate limit%AM10%",
170
- "%xyzzy server%",
171
- "%Quantum entanglement enables FTL%",
172
- "%Install Docker%AM10%",
173
- "%normal safe content about coding%",
174
- "%test diary%",
175
- "%test critique%",
176
- "%integration test diary%",
177
- ]
178
- for pattern in test_patterns:
179
- cur = db.execute(
180
- "DELETE FROM stm_memories WHERE content LIKE ? "
181
- "AND (lifecycle_state IS NULL OR lifecycle_state != 'pinned')",
182
- (pattern,)
183
- )
184
- deleted += cur.rowcount or 0
185
-
186
- if deleted > 0:
187
- db.commit()
188
- return deleted
189
-
190
- def gc_sensory(max_age_hours: int = 48) -> int:
191
- """Garbage collect sensory memories older than max_age_hours. Returns count deleted."""
192
- db = _get_db()
193
- cutoff = (datetime.utcnow() - timedelta(hours=max_age_hours)).isoformat()
194
- cur = db.execute(
195
- "DELETE FROM stm_memories WHERE source_type = 'sensory' AND created_at < ? AND promoted_to_ltm = 0",
196
- (cutoff,)
197
- )
198
- db.commit()
199
- return cur.rowcount or 0
200
-
201
-
202
- def gc_ltm_dormant(min_age_days: int = 30) -> int:
203
- """Delete dormant LTM memories with strength < 0.1 older than min_age_days."""
204
- db = _get_db()
205
- cutoff = (datetime.utcnow() - timedelta(days=min_age_days)).isoformat()
206
- cur = db.execute(
207
- "DELETE FROM ltm_memories WHERE is_dormant = 1 AND strength < 0.1 AND created_at < ?",
208
- (cutoff,)
209
- )
210
- db.commit()
211
- return cur.rowcount or 0
212
-
213
- def dream_cycle(max_insights: int = 50) -> dict:
214
- """Memory Dreaming — discover hidden connections between recent memories.
215
-
216
- Retrieves memories accessed in the last 24h (STM + LTM), finds pairs with
217
- moderate similarity (0.4-0.7 — related but not duplicates), and creates
218
- 'dream_insight' LTM memories linking them. Skips pairs already dreamed about.
219
-
220
- Uses pure vector math — no LLM calls.
221
-
222
- Returns:
223
- Dict with 'insights_created' count and 'insights' list of details.
224
- """
225
- db = _get_db()
226
- cutoff_24h = (datetime.utcnow() - timedelta(hours=24)).isoformat()
227
-
228
- # 1. Gather all memories accessed in the last 24 hours
229
- recent_memories = []
230
-
231
- stm_rows = db.execute(
232
- """SELECT id, content, embedding, source_type, source_title, domain, 'stm' as store
233
- FROM stm_memories
234
- WHERE last_accessed >= ? AND promoted_to_ltm = 0""",
235
- (cutoff_24h,)
236
- ).fetchall()
237
-
238
- ltm_rows = db.execute(
239
- """SELECT id, content, embedding, source_type, source_title, domain, 'ltm' as store
240
- FROM ltm_memories
241
- WHERE last_accessed >= ? AND is_dormant = 0""",
242
- (cutoff_24h,)
243
- ).fetchall()
244
-
245
- for row in stm_rows + ltm_rows:
246
- recent_memories.append({
247
- "id": row["id"],
248
- "content": row["content"],
249
- "vec": _blob_to_array(row["embedding"]),
250
- "source_type": row["source_type"],
251
- "source_title": row["source_title"] or "",
252
- "domain": row["domain"] or "",
253
- "store": row["store"],
254
- })
255
-
256
- if len(recent_memories) < 2:
257
- return {"insights_created": 0, "insights": [], "memories_scanned": len(recent_memories), "candidates_found": 0}
258
-
259
- # 2. Get already-dreamed pairs to skip
260
- dreamed = set()
261
- for row in db.execute("SELECT memory_a_id, memory_b_id FROM dreamed_pairs").fetchall():
262
- dreamed.add((row["memory_a_id"], row["memory_b_id"]))
263
- dreamed.add((row["memory_b_id"], row["memory_a_id"]))
264
-
265
- # 3. Batch compute all pairwise cosine similarities
266
- # Build matrix for fast numpy dot product
267
- n = len(recent_memories)
268
- vecs = np.array([m["vec"] for m in recent_memories], dtype=np.float32)
269
- norms = np.linalg.norm(vecs, axis=1, keepdims=True)
270
- norms[norms == 0] = 1.0 # avoid division by zero
271
- normalized = vecs / norms
272
- sim_matrix = normalized @ normalized.T # (n x n) cosine similarity matrix
273
-
274
- # 4. Find pairs in the sweet spot (0.4-0.7) — related but not duplicates
275
- candidate_pairs = []
276
- for i in range(n):
277
- for j in range(i + 1, n):
278
- score = float(sim_matrix[i, j])
279
- if 0.4 <= score <= 0.7:
280
- # Use composite key for dreamed check (store:id to disambiguate stm vs ltm)
281
- pair_key = (
282
- f"{recent_memories[i]['store']}:{recent_memories[i]['id']}",
283
- f"{recent_memories[j]['store']}:{recent_memories[j]['id']}",
284
- )
285
- # For DB tracking we use LTM IDs when both are LTM, else skip dreamed check
286
- a_id, b_id = recent_memories[i]["id"], recent_memories[j]["id"]
287
- if (a_id, b_id) in dreamed or (b_id, a_id) in dreamed:
288
- continue
289
- candidate_pairs.append((i, j, score))
290
-
291
- # Sort by similarity descending (strongest connections first)
292
- candidate_pairs.sort(key=lambda x: x[2], reverse=True)
293
-
294
- # 5. Generate insights (capped at max_insights)
295
- insights = []
296
- for i, j, score in candidate_pairs[:max_insights]:
297
- mem_a = recent_memories[i]
298
- mem_b = recent_memories[j]
299
-
300
- # Build titles — use source_title if available, else first 60 chars of content
301
- title_a = mem_a["source_title"] or mem_a["content"][:60].replace("\n", " ").strip()
302
- title_b = mem_b["source_title"] or mem_b["content"][:60].replace("\n", " ").strip()
303
-
304
- # Build domain context
305
- domains = set(filter(None, [mem_a["domain"], mem_b["domain"]]))
306
- domain_str = ", ".join(domains) if domains else "general"
307
-
308
- # Create insight content
309
- insight_content = (
310
- f"[Dream Insight] Connection found between:\n"
311
- f" A: {title_a}\n"
312
- f" B: {title_b}\n"
313
- f"Similarity: {score:.3f} | Domains: {domain_str}\n"
314
- f"These memories appeared together in the same 24h window and share moderate semantic overlap, "
315
- f"suggesting a potential relationship worth investigating."
316
- )
317
-
318
- # Create embedding as average of the two source vectors (midpoint in vector space)
319
- insight_vec = (mem_a["vec"] + mem_b["vec"]) / 2.0
320
- insight_vec = insight_vec / (np.linalg.norm(insight_vec) or 1.0) # re-normalize
321
- blob = _array_to_blob(insight_vec)
322
-
323
- # Store as LTM with dream_insight tag
324
- cur = db.execute(
325
- """INSERT INTO ltm_memories (content, embedding, source_type, source_id, source_title, domain, tags, strength)
326
- VALUES (?, ?, 'dream_insight', ?, ?, ?, 'dream_insight', 0.5)""",
327
- (insight_content, blob,
328
- f"{mem_a['store']}:{mem_a['id']},{mem_b['store']}:{mem_b['id']}",
329
- f"Dream: {title_a[:30]} <-> {title_b[:30]}",
330
- domain_str)
331
- )
332
- insight_id = cur.lastrowid
333
-
334
- # Track the dreamed pair
335
- a_id, b_id = mem_a["id"], mem_b["id"]
336
- try:
337
- db.execute(
338
- "INSERT OR IGNORE INTO dreamed_pairs (memory_a_id, memory_b_id, insight_id) VALUES (?, ?, ?)",
339
- (min(a_id, b_id), max(a_id, b_id), insight_id)
340
- )
341
- except Exception:
342
- pass
343
-
344
- insights.append({
345
- "insight_id": insight_id,
346
- "title_a": title_a[:80],
347
- "title_b": title_b[:80],
348
- "similarity": round(score, 4),
349
- "domain": domain_str,
350
- })
351
-
352
- db.commit()
353
-
354
- # Dream cap: archive oldest dream_insights if total exceeds MAX_DREAM_INSIGHTS
355
- MAX_DREAM_INSIGHTS = 50
356
- dream_count = db.execute(
357
- "SELECT COUNT(*) FROM ltm_memories WHERE source_type = 'dream_insight' AND is_dormant = 0"
358
- ).fetchone()[0]
359
- archived_dreams = 0
360
- if dream_count > MAX_DREAM_INSIGHTS:
361
- excess = dream_count - MAX_DREAM_INSIGHTS
362
- oldest = db.execute(
363
- "SELECT id FROM ltm_memories WHERE source_type = 'dream_insight' AND is_dormant = 0 "
364
- "ORDER BY strength ASC, created_at ASC LIMIT ?", (excess,)
365
- ).fetchall()
366
- for row in oldest:
367
- db.execute(
368
- "UPDATE ltm_memories SET lifecycle_state = 'archived' WHERE id = ?", (row["id"],)
369
- )
370
- archived_dreams += 1
371
- db.commit()
372
-
373
- result = {
374
- "insights_created": len(insights),
375
- "insights": insights,
376
- "memories_scanned": len(recent_memories),
377
- "candidates_found": len(candidate_pairs),
378
- }
379
- if archived_dreams > 0:
380
- result["dreams_archived"] = archived_dreams
381
- result["dreams_total"] = dream_count
382
- return result