nexo-brain 5.3.20 → 5.3.21

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 (210) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/src/auto_update.py +11 -8
  4. package/src/dashboard/static/favicon 2.svg +32 -0
  5. package/src/dashboard/static/nexo-logo 2.png +0 -0
  6. package/src/dashboard/static/nexo-logo 2.svg +40 -0
  7. package/src/dashboard/static/style 2.css +2458 -0
  8. package/src/dashboard/templates/adaptive 2.html +118 -0
  9. package/src/dashboard/templates/artifacts 2.html +133 -0
  10. package/src/dashboard/templates/backups 2.html +136 -0
  11. package/src/dashboard/templates/base 2.html +417 -0
  12. package/src/dashboard/templates/calendar 2.html +591 -0
  13. package/src/dashboard/templates/chat 2.html +356 -0
  14. package/src/dashboard/templates/claims 2.html +259 -0
  15. package/src/dashboard/templates/cortex 2.html +321 -0
  16. package/src/dashboard/templates/credentials 2.html +128 -0
  17. package/src/dashboard/templates/crons 2.html +370 -0
  18. package/src/dashboard/templates/dashboard 2.html +494 -0
  19. package/src/dashboard/templates/dreams 2.html +252 -0
  20. package/src/dashboard/templates/email 2.html +160 -0
  21. package/src/dashboard/templates/evolution 2.html +189 -0
  22. package/src/dashboard/templates/feed 2.html +249 -0
  23. package/src/dashboard/templates/followup_health 2.html +170 -0
  24. package/src/dashboard/templates/graph 2.html +201 -0
  25. package/src/dashboard/templates/guard 2.html +259 -0
  26. package/src/dashboard/templates/inbox 2.html +251 -0
  27. package/src/dashboard/templates/memory 2.html +420 -0
  28. package/src/dashboard/templates/operations 2.html +608 -0
  29. package/src/dashboard/templates/plugins 2.html +185 -0
  30. package/src/dashboard/templates/protocol 2.html +199 -0
  31. package/src/dashboard/templates/rules 2.html +246 -0
  32. package/src/dashboard/templates/sentiment 2.html +247 -0
  33. package/src/dashboard/templates/sessions 2.html +218 -0
  34. package/src/dashboard/templates/skills 2.html +329 -0
  35. package/src/dashboard/templates/somatic 2.html +73 -0
  36. package/src/dashboard/templates/triggers 2.html +133 -0
  37. package/src/dashboard/templates/trust 2.html +360 -0
  38. package/src/db/__init__ 2.py +259 -0
  39. package/src/db/_core 2.py +437 -0
  40. package/src/db/_credentials 2.py +124 -0
  41. package/src/db/_episodic 2.py +762 -0
  42. package/src/db/_evolution 2.py +54 -0
  43. package/src/db/_fts 2.py +406 -0
  44. package/src/db/_goal_profiles 2.py +376 -0
  45. package/src/db/_hot_context 2.py +660 -0
  46. package/src/db/_outcomes 2.py +800 -0
  47. package/src/db/_personal_scripts 2.py +582 -0
  48. package/src/db/_sessions 2.py +330 -0
  49. package/src/db/_tasks 2.py +91 -0
  50. package/src/db/_watchers 2.py +173 -0
  51. package/src/doctor/formatters 2.py +52 -0
  52. package/src/doctor/models 2.py +69 -0
  53. package/src/doctor/planes 2.py +87 -0
  54. package/src/doctor/providers/__init__ 2.py +1 -0
  55. package/src/doctor/providers/deep 2.py +367 -0
  56. package/src/evolution_cycle 2.py +519 -0
  57. package/src/hooks/auto_capture 2.py +208 -0
  58. package/src/hooks/caffeinate-guard 2.sh +8 -0
  59. package/src/hooks/capture-session 2.sh +21 -0
  60. package/src/hooks/capture-tool-logs 2.sh +158 -0
  61. package/src/hooks/daily-briefing-check 2.sh +33 -0
  62. package/src/hooks/heartbeat-enforcement 2.py +90 -0
  63. package/src/hooks/heartbeat-posttool 2.sh +18 -0
  64. package/src/hooks/inbox-hook 2.sh +76 -0
  65. package/src/hooks/post-compact 2.sh +152 -0
  66. package/src/hooks/pre-compact 2.sh +169 -0
  67. package/src/hooks/protocol-guardrail 2.sh +10 -0
  68. package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
  69. package/src/hooks/session-stop 2.sh +52 -0
  70. package/src/kg_populate 2.py +292 -0
  71. package/src/maintenance 2.py +53 -0
  72. package/src/memory_backends 2.py +71 -0
  73. package/src/migrate_embeddings 2.py +124 -0
  74. package/src/nexo_sdk 2.py +103 -0
  75. package/src/observability 2.py +199 -0
  76. package/src/plugin_loader 2.py +217 -0
  77. package/src/plugins/__init__ 2.py +0 -0
  78. package/src/plugins/artifact_registry 2.py +450 -0
  79. package/src/plugins/backup 2.py +127 -0
  80. package/src/plugins/claims_tools 2.py +119 -0
  81. package/src/plugins/cognitive_memory 2.py +609 -0
  82. package/src/plugins/core_rules 2.py +252 -0
  83. package/src/plugins/cortex 2.py +1155 -0
  84. package/src/plugins/entities 2.py +67 -0
  85. package/src/plugins/episodic_memory 2.py +560 -0
  86. package/src/plugins/evolution 2.py +167 -0
  87. package/src/plugins/goal_engine 2.py +142 -0
  88. package/src/plugins/guard 2.py +862 -0
  89. package/src/plugins/impact 2.py +29 -0
  90. package/src/plugins/knowledge_graph_tools 2.py +137 -0
  91. package/src/plugins/media_memory_tools 2.py +98 -0
  92. package/src/plugins/memory_export 2.py +196 -0
  93. package/src/plugins/outcomes 2.py +130 -0
  94. package/src/plugins/personal_scripts 2.py +117 -0
  95. package/src/plugins/preferences 2.py +47 -0
  96. package/src/plugins/protocol 2.py +1449 -0
  97. package/src/plugins/simple_api 2.py +106 -0
  98. package/src/plugins/skills 2.py +341 -0
  99. package/src/plugins/state_watchers 2.py +79 -0
  100. package/src/plugins/update 2.py +986 -0
  101. package/src/plugins/user_state_tools 2.py +43 -0
  102. package/src/plugins/workflow 2.py +588 -0
  103. package/src/protocol_settings 2.py +59 -0
  104. package/src/public_contribution 2.py +466 -0
  105. package/src/public_evolution_queue 2.py +241 -0
  106. package/src/requirements 2.txt +14 -0
  107. package/src/retroactive_learnings 2.py +373 -0
  108. package/src/rules/__init__ 2.py +0 -0
  109. package/src/rules/core-rules 2.json +331 -0
  110. package/src/rules/migrate 2.py +207 -0
  111. package/src/runtime_power 2.py +874 -0
  112. package/src/script_registry 2.py +1559 -0
  113. package/src/scripts/check-context 2.py +272 -0
  114. package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
  115. package/src/scripts/deep-sleep/collect 2.py +928 -0
  116. package/src/scripts/deep-sleep/extract 2.py +330 -0
  117. package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
  118. package/src/scripts/deep-sleep/synthesize 2.py +312 -0
  119. package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
  120. package/src/scripts/nexo-agent-run 2.py +75 -0
  121. package/src/scripts/nexo-auto-update 2.py +6 -0
  122. package/src/scripts/nexo-backup 2.sh +25 -0
  123. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  124. package/src/scripts/nexo-catchup 2.py +300 -0
  125. package/src/scripts/nexo-cognitive-decay 2.py +257 -0
  126. package/src/scripts/nexo-cortex-cycle 2.py +293 -0
  127. package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
  128. package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
  129. package/src/scripts/nexo-dashboard 2.sh +29 -0
  130. package/src/scripts/nexo-deep-sleep 2.sh +86 -0
  131. package/src/scripts/nexo-evolution-run 2.py +1664 -0
  132. package/src/scripts/nexo-followup-hygiene 2.py +139 -0
  133. package/src/scripts/nexo-hook-record 2.py +42 -0
  134. package/src/scripts/nexo-immune 2.py +936 -0
  135. package/src/scripts/nexo-impact-scorer 2.py +117 -0
  136. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  137. package/src/scripts/nexo-install 2.py +6 -0
  138. package/src/scripts/nexo-learning-housekeep 2.py +401 -0
  139. package/src/scripts/nexo-learning-validator 2.py +266 -0
  140. package/src/scripts/nexo-migrate 2.py +260 -0
  141. package/src/scripts/nexo-outcome-checker 2.py +127 -0
  142. package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
  143. package/src/scripts/nexo-pre-commit 2.py +120 -0
  144. package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
  145. package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
  146. package/src/scripts/nexo-reflection 2.py +256 -0
  147. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  148. package/src/scripts/nexo-sleep 2.py +631 -0
  149. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  150. package/src/scripts/nexo-sync-clients 2.py +16 -0
  151. package/src/scripts/nexo-synthesis 2.py +475 -0
  152. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  153. package/src/scripts/nexo-update 2.sh +306 -0
  154. package/src/scripts/nexo-watchdog 2.sh +1207 -0
  155. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  156. package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
  157. package/src/server 2.py +1296 -0
  158. package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
  159. package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
  160. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
  161. package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
  162. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
  163. package/src/skills/run-release-final-audit/guide 2.md +16 -0
  164. package/src/skills/run-release-final-audit/script 2.py +259 -0
  165. package/src/skills/run-release-final-audit/skill 2.json +77 -0
  166. package/src/skills/run-runtime-doctor/guide 2.md +12 -0
  167. package/src/skills/run-runtime-doctor/script 2.py +21 -0
  168. package/src/skills/run-runtime-doctor/skill 2.json +25 -0
  169. package/src/skills_runtime 2.py +932 -0
  170. package/src/state_watchers_runtime 2.py +475 -0
  171. package/src/storage_router 2.py +32 -0
  172. package/src/system_catalog 2.py +786 -0
  173. package/src/tools_coordination 2.py +103 -0
  174. package/src/tools_credentials 2.py +68 -0
  175. package/src/tools_drive 2.py +487 -0
  176. package/src/tools_hot_context 2.py +163 -0
  177. package/src/tools_learnings 2.py +612 -0
  178. package/src/tools_menu 2.py +229 -0
  179. package/src/tools_reminders 2.py +88 -0
  180. package/src/tools_reminders_crud 2.py +363 -0
  181. package/src/tools_sessions 2.py +1054 -0
  182. package/src/tools_system_catalog 2.py +19 -0
  183. package/src/tools_task_history 2.py +57 -0
  184. package/src/tools_transcripts 2.py +98 -0
  185. package/src/transcript_utils 2.py +412 -0
  186. package/src/user_context 2.py +46 -0
  187. package/src/user_data_portability 2.py +328 -0
  188. package/src/user_state_model 2.py +170 -0
  189. package/templates/CLAUDE.md 2.template +108 -0
  190. package/templates/CODEX.AGENTS.md 2.template +66 -0
  191. package/templates/launchagents/README 2.md +132 -0
  192. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
  193. package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
  194. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
  195. package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
  196. package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
  197. package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
  198. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
  199. package/templates/launchagents/com.nexo.immune 2.plist +41 -0
  200. package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
  201. package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
  202. package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
  203. package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
  204. package/templates/nexo_helper 2.py +301 -0
  205. package/templates/openclaw 2.json +13 -0
  206. package/templates/plugin-template 2.py +40 -0
  207. package/templates/script-template 2.py +59 -0
  208. package/templates/script-template 2.sh +13 -0
  209. package/templates/skill-script-template 2.py +48 -0
  210. package/templates/skill-template 2.md +33 -0
@@ -0,0 +1,29 @@
1
+ """Impact scoring plugin — prioritize followups by expected impact, not only by date."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from db import get_followup, score_followup
8
+
9
+
10
+ def handle_impact_score(followup_id: str) -> str:
11
+ """Compute and persist Impact Scoring v1 for one followup."""
12
+ row = get_followup(followup_id)
13
+ if not row:
14
+ return f"ERROR: Followup {followup_id} not found."
15
+ scored = score_followup(followup_id)
16
+ if "error" in scored:
17
+ return f"ERROR: {scored['error']}"
18
+ payload = {
19
+ "followup_id": followup_id,
20
+ "impact_score": scored.get("impact_score", 0),
21
+ "factors": scored.get("impact_factors", {}),
22
+ "reasoning": scored.get("impact_reasoning", ""),
23
+ }
24
+ return json.dumps(payload, ensure_ascii=False, indent=2)
25
+
26
+
27
+ TOOLS = [
28
+ (handle_impact_score, "nexo_impact_score", "Compute and persist Impact Scoring v1 for a followup so queues can prioritize by expected impact."),
29
+ ]
@@ -0,0 +1,137 @@
1
+ """Knowledge Graph MCP tools — query, path, neighbors, stats."""
2
+ import os
3
+ import sys
4
+
5
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
6
+ import knowledge_graph as kg
7
+
8
+
9
+ def _find_node(node_type: str, node_ref: str):
10
+ """Find a node, trying both raw ref and type-prefixed ref (area:X, file:X)."""
11
+ node = kg.get_node(node_type, node_ref)
12
+ if not node:
13
+ node = kg.get_node(node_type, f"{node_type}:{node_ref}")
14
+ return node
15
+
16
+
17
+ def handle_kg_query(node_type: str, node_ref: str, depth: int = 2, relation: str = "") -> str:
18
+ """Traverse the knowledge graph from a node up to `depth` hops."""
19
+ node = _find_node(node_type, node_ref)
20
+ if not node:
21
+ return f"Node not found: {node_type}/{node_ref}"
22
+ result = kg.traverse(node["id"], max_depth=depth, relation_filter=relation or None)
23
+ nodes = result["nodes"][:30]
24
+ edges = result["edges"][:30]
25
+ lines = [f"KG TRAVERSE — {node['label']} ({node_type}/{node_ref}) depth={depth}"]
26
+ lines.append(f"Nodes: {len(result['nodes'])} Edges: {len(result['edges'])}")
27
+ lines.append("")
28
+ lines.append("NODES:")
29
+ for n in nodes:
30
+ indent = " " * n.get("depth", 0)
31
+ lines.append(f" {indent}[{n['id']}] ({n['node_type']}) {n['label']} — {n['node_ref']}")
32
+ lines.append("")
33
+ lines.append("EDGES:")
34
+ for e in edges[:20]:
35
+ lines.append(f" [{e['source_id']}] --{e['relation']}--> [{e['target_id']}] w={e['weight']}")
36
+ return "\n".join(lines)
37
+
38
+
39
+ def handle_kg_path(from_type: str, from_ref: str, to_type: str, to_ref: str) -> str:
40
+ """Find the shortest path between two nodes in the knowledge graph."""
41
+ from_node = _find_node(from_type, from_ref)
42
+ if not from_node:
43
+ return f"Source node not found: {from_type}/{from_ref}"
44
+ to_node = _find_node(to_type, to_ref)
45
+ if not to_node:
46
+ return f"Target node not found: {to_type}/{to_ref}"
47
+ path_ids = kg.shortest_path(from_node["id"], to_node["id"])
48
+ if not path_ids:
49
+ return f"No path found between {from_ref} and {to_ref}"
50
+ lines = [f"PATH ({len(path_ids) - 1} hops): {from_ref} → {to_ref}"]
51
+ for i, nid in enumerate(path_ids):
52
+ node = kg.get_node_by_id(nid)
53
+ label = node["label"] if node else f"[{nid}]"
54
+ ntype = node["node_type"] if node else "?"
55
+ lines.append(f" {i}. [{nid}] ({ntype}) {label}")
56
+ return "\n".join(lines)
57
+
58
+
59
+ def handle_kg_neighbors(node_type: str, node_ref: str, relation: str = "") -> str:
60
+ """Get direct neighbors of a node, optionally filtered by relation type."""
61
+ node = _find_node(node_type, node_ref)
62
+ if not node:
63
+ return f"Node not found: {node_type}/{node_ref}"
64
+ neighbors = kg.get_neighbors(node["id"], relation=relation or None)
65
+ if not neighbors:
66
+ rel_info = f" (relation={relation})" if relation else ""
67
+ return f"No neighbors found for {node['label']}{rel_info}"
68
+ lines = [f"NEIGHBORS of [{node['id']}] {node['label']} ({len(neighbors)} total):"]
69
+ for n in neighbors[:30]:
70
+ direction = n.get("direction", "?")
71
+ arrow = "-->" if direction == "outgoing" else "<--"
72
+ lines.append(f" {arrow} [{n['id']}] {n['label']} ({n['node_type']}) rel={n['relation']} w={n['weight']}")
73
+ if len(neighbors) > 30:
74
+ lines.append(f" ... +{len(neighbors) - 30} more")
75
+ return "\n".join(lines)
76
+
77
+
78
+ def handle_kg_stats() -> str:
79
+ """Return knowledge graph statistics: node counts, edge counts, top connected nodes."""
80
+ s = kg.stats()
81
+ lines = ["KNOWLEDGE GRAPH STATS"]
82
+ lines.append(f" Nodes: {s['nodes']}")
83
+ lines.append(f" Edges (active): {s['edges_active']}")
84
+ lines.append(f" Edges (historical): {s['edges_historical']}")
85
+ if s["node_types"]:
86
+ lines.append("\nNODE TYPES:")
87
+ for t, cnt in sorted(s["node_types"].items(), key=lambda x: -x[1]):
88
+ lines.append(f" {t}: {cnt}")
89
+ if s["relation_types"]:
90
+ lines.append("\nRELATION TYPES:")
91
+ for r, cnt in sorted(s["relation_types"].items(), key=lambda x: -x[1])[:20]:
92
+ lines.append(f" {r}: {cnt}")
93
+ if s["most_connected"]:
94
+ lines.append("\nMOST CONNECTED:")
95
+ for n in s["most_connected"][:10]:
96
+ lines.append(f" [{n['id']}] {n['label']} ({n['node_type']}) — {n['connections']} connections")
97
+ return "\n".join(lines)
98
+
99
+
100
+ def handle_kg_export(format: str = "jsonld", as_of: str = "") -> str:
101
+ """Export the bitemporal knowledge graph to a standard interchange format.
102
+
103
+ Closes Fase 5 item 1 of NEXO-AUDIT-2026-04-11. The KG was already
104
+ bitemporal (kg_edges has valid_from and valid_until and the
105
+ upsert/delete helpers maintain them), but had no way to emit the
106
+ graph in a format external tools can ingest. This tool wraps the
107
+ two canonical exporters in cognitive.knowledge_graph.
108
+
109
+ Args:
110
+ format: 'jsonld' (default, semantic web / human-readable) or
111
+ 'graphml' (igraph, Gephi, NetworkX, Cytoscape).
112
+ as_of: Optional ISO timestamp. If empty, exports the active
113
+ snapshot. If provided, exports the historical snapshot
114
+ that was valid at that instant.
115
+ """
116
+ import json as _json
117
+ import knowledge_graph as kg
118
+
119
+ fmt = (format or "jsonld").strip().lower()
120
+ if fmt == "jsonld":
121
+ payload = kg.export_to_jsonld(as_of=as_of)
122
+ return _json.dumps(payload, ensure_ascii=False, indent=2)
123
+ if fmt == "graphml":
124
+ return kg.export_to_graphml(as_of=as_of)
125
+ return _json.dumps(
126
+ {"ok": False, "error": f"unsupported format: {format!r} (use jsonld or graphml)"},
127
+ ensure_ascii=False,
128
+ )
129
+
130
+
131
+ TOOLS = [
132
+ (handle_kg_query, "nexo_kg_query", "Query knowledge graph — traverse from a node"),
133
+ (handle_kg_path, "nexo_kg_path", "Find shortest path between two nodes"),
134
+ (handle_kg_neighbors, "nexo_kg_neighbors", "Get direct neighbors of a node"),
135
+ (handle_kg_stats, "nexo_kg_stats", "Knowledge graph statistics"),
136
+ (handle_kg_export, "nexo_kg_export", "Export the bitemporal KG to JSON-LD or GraphML (active snapshot or historical via as_of)"),
137
+ ]
@@ -0,0 +1,98 @@
1
+ """Multimodal memory reference tools."""
2
+
3
+ import media_memory
4
+
5
+
6
+ def handle_media_memory_add(
7
+ file_path: str = "",
8
+ url: str = "",
9
+ title: str = "",
10
+ description: str = "",
11
+ tags: str = "",
12
+ domain: str = "",
13
+ source_type: str = "",
14
+ source_id: str = "",
15
+ metadata: str = "",
16
+ ) -> str:
17
+ result = media_memory.add_media_memory(
18
+ file_path=file_path,
19
+ url=url,
20
+ title=title,
21
+ description=description,
22
+ tags=tags,
23
+ domain=domain,
24
+ source_type=source_type,
25
+ source_id=source_id,
26
+ metadata=metadata,
27
+ )
28
+ if result.get("error"):
29
+ return f"ERROR: {result['error']}"
30
+ location = result.get("file_path") or result.get("url") or "n/a"
31
+ return f"Media memory #{result['id']} [{result['media_type']}] stored: {location}"
32
+
33
+
34
+ def handle_media_memory_search(
35
+ query: str = "",
36
+ media_type: str = "",
37
+ domain: str = "",
38
+ tag: str = "",
39
+ limit: int = 20,
40
+ ) -> str:
41
+ items = media_memory.search_media_memories(
42
+ query=query,
43
+ media_type=media_type,
44
+ domain=domain,
45
+ tag=tag,
46
+ limit=limit,
47
+ )
48
+ if not items:
49
+ return "No media memories found."
50
+ lines = [f"MEDIA MEMORIES — {len(items)} result(s):", ""]
51
+ for item in items:
52
+ lines.append(f" #{item['id']} [{item['media_type']}] {item['title'][:120]}")
53
+ lines.append(f" {item.get('file_path') or item.get('url') or 'n/a'}")
54
+ if item.get("description"):
55
+ lines.append(f" {item['description'][:180]}")
56
+ if item.get("tags"):
57
+ lines.append(f" tags: {item['tags']}")
58
+ return "\n".join(lines)
59
+
60
+
61
+ def handle_media_memory_get(media_id: int) -> str:
62
+ item = media_memory.get_media_memory(media_id)
63
+ if not item:
64
+ return f"Media memory #{media_id} not found."
65
+ lines = [
66
+ f"MEDIA MEMORY #{item['id']}",
67
+ f" type: {item['media_type']}",
68
+ f" title: {item['title']}",
69
+ f" location: {item.get('file_path') or item.get('url') or 'n/a'}",
70
+ f" domain: {item.get('domain') or 'n/a'}",
71
+ f" source: {item.get('source_type') or 'n/a'}:{item.get('source_id') or ''}",
72
+ ]
73
+ if item.get("description"):
74
+ lines.append(f" description: {item['description']}")
75
+ if item.get("tags"):
76
+ lines.append(f" tags: {item['tags']}")
77
+ if item.get("metadata"):
78
+ lines.append(f" metadata: {item['metadata']}")
79
+ return "\n".join(lines)
80
+
81
+
82
+ def handle_media_memory_stats() -> str:
83
+ stats = media_memory.media_memory_stats()
84
+ return (
85
+ "MEDIA MEMORY STATS\n"
86
+ f" total: {stats['total']}\n"
87
+ f" backend: {stats['backend']}\n"
88
+ f" by_type: {stats['by_type']}\n"
89
+ f" by_domain: {stats['by_domain']}"
90
+ )
91
+
92
+
93
+ TOOLS = [
94
+ (handle_media_memory_add, "nexo_media_memory_add", "Store a non-text artifact as first-class media memory metadata."),
95
+ (handle_media_memory_search, "nexo_media_memory_search", "Search media memories by text, type, tag, or domain."),
96
+ (handle_media_memory_get, "nexo_media_memory_get", "Inspect one stored media memory."),
97
+ (handle_media_memory_stats, "nexo_media_memory_stats", "Stats for the multimodal/media memory layer."),
98
+ ]
@@ -0,0 +1,196 @@
1
+ """Readable export and auto-flush inspection tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ import cognitive
11
+ import claim_graph
12
+ import compaction_memory
13
+ import media_memory
14
+ import user_state_model
15
+ from db import get_db
16
+ from memory_backends import get_backend, list_backends
17
+
18
+
19
+ def _nexo_home() -> Path:
20
+ return Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))).expanduser()
21
+
22
+
23
+ def _write(path: Path, text: str) -> None:
24
+ path.parent.mkdir(parents=True, exist_ok=True)
25
+ path.write_text(text, encoding="utf-8")
26
+
27
+
28
+ def handle_auto_flush_recent(limit: int = 20, session_id: str = "") -> str:
29
+ rows = compaction_memory.list_auto_flushes(session_id=session_id, limit=limit)
30
+ if not rows:
31
+ return "No auto-flush records."
32
+ lines = [f"AUTO-FLUSH — {len(rows)} record(s):", ""]
33
+ for row in rows:
34
+ lines.append(f" #{row['id']} {row['created_at']} [{row.get('session_id','unknown')}]")
35
+ lines.append(f" {row.get('summary','')[:220]}")
36
+ if row.get("next_step"):
37
+ lines.append(f" next: {row['next_step'][:160]}")
38
+ return "\n".join(lines)
39
+
40
+
41
+ def handle_auto_flush_stats(days: int = 7) -> str:
42
+ stats = compaction_memory.auto_flush_stats(days=days)
43
+ return (
44
+ f"AUTO-FLUSH STATS — {stats['window_days']}d\n"
45
+ f" total: {stats['total']}\n"
46
+ f" backend: {stats['backend']}\n"
47
+ f" by_source: {stats['by_source']}"
48
+ )
49
+
50
+
51
+ def handle_memory_backend_status() -> str:
52
+ active = get_backend()
53
+ backends = list_backends()
54
+ lines = [
55
+ f"MEMORY BACKEND: {active.key} — {active.label}",
56
+ f"Description: {active.description}",
57
+ f"Supports: {', '.join(active.supports)}",
58
+ "",
59
+ "Registered backends:",
60
+ ]
61
+ for item in backends:
62
+ marker = "*" if item["active"] else "-"
63
+ lines.append(f" {marker} {item['key']} [{item['maturity']}] {item['label']}")
64
+ return "\n".join(lines)
65
+
66
+
67
+ def handle_memory_export(format: str = "markdown", output_dir: str = "") -> str:
68
+ if format.strip().lower() != "markdown":
69
+ return "ERROR: only markdown export is supported for now."
70
+
71
+ stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
72
+ root = Path(output_dir).expanduser() if output_dir.strip() else (_nexo_home() / "exports" / "memory" / stamp)
73
+ root.mkdir(parents=True, exist_ok=True)
74
+
75
+ conn = get_db()
76
+ learnings = [dict(r) for r in conn.execute("SELECT id, category, title, status, prevention, updated_at FROM learnings ORDER BY updated_at DESC LIMIT 50").fetchall()]
77
+ decisions = [dict(r) for r in conn.execute("SELECT id, domain, decision, confidence, status, created_at FROM decisions ORDER BY created_at DESC LIMIT 50").fetchall()]
78
+ claims = claim_graph.search_claims(limit=100)
79
+ claim_lint = claim_graph.lint_claims(limit=50)
80
+ media = media_memory.list_media_memories(limit=100)
81
+ flushes = compaction_memory.list_auto_flushes(limit=100)
82
+ user_state = user_state_model.build_user_state(days=7, persist=False)
83
+ user_history = user_state_model.list_user_state_snapshots(limit=30)
84
+ cognitive_stats = cognitive.get_stats()
85
+
86
+ _write(
87
+ root / "README.md",
88
+ "\n".join(
89
+ [
90
+ "# NEXO Memory Export",
91
+ "",
92
+ f"- Generated: {datetime.now().isoformat(timespec='seconds')}",
93
+ f"- Backend: {get_backend().key}",
94
+ f"- Learnings: {len(learnings)}",
95
+ f"- Decisions: {len(decisions)}",
96
+ f"- Claims: {len(claims)}",
97
+ f"- Media memories: {len(media)}",
98
+ f"- Auto-flush records: {len(flushes)}",
99
+ "",
100
+ "Files:",
101
+ "- `learnings.md`",
102
+ "- `decisions.md`",
103
+ "- `claims.md`",
104
+ "- `media.md`",
105
+ "- `auto-flush.md`",
106
+ "- `user-state.md`",
107
+ "- `cognitive.json`",
108
+ ]
109
+ ),
110
+ )
111
+ _write(
112
+ root / "learnings.md",
113
+ "\n".join(
114
+ ["# Learnings", ""]
115
+ + [
116
+ f"- #{item['id']} [{item.get('category','general')}] {item['title']} "
117
+ f"({item.get('status','active')}, updated {item.get('updated_at','')})"
118
+ for item in learnings
119
+ ]
120
+ ),
121
+ )
122
+ _write(
123
+ root / "decisions.md",
124
+ "\n".join(
125
+ ["# Decisions", ""]
126
+ + [
127
+ f"- #{item['id']} [{item.get('domain','other')}] {item['decision']} "
128
+ f"({item.get('confidence','medium')}, {item.get('status','pending_review')})"
129
+ for item in decisions
130
+ ]
131
+ ),
132
+ )
133
+ _write(
134
+ root / "claims.md",
135
+ "\n".join(
136
+ ["# Claims", ""]
137
+ + [
138
+ f"- #{item['id']} [{item.get('verification_status','unverified')}] "
139
+ f"{item.get('freshness_state','?')}({item.get('freshness_score',0)}): {item['text']}"
140
+ for item in claims
141
+ ]
142
+ + ["", "## Attention", ""]
143
+ + [
144
+ f"- #{item['id']} [{', '.join(item.get('lint_reasons', []))}] {item['text']}"
145
+ for item in claim_lint
146
+ ]
147
+ ),
148
+ )
149
+ _write(
150
+ root / "media.md",
151
+ "\n".join(
152
+ ["# Media Memory", ""]
153
+ + [
154
+ f"- #{item['id']} [{item['media_type']}] {item['title']} :: {item.get('file_path') or item.get('url') or 'n/a'}"
155
+ for item in media
156
+ ]
157
+ ),
158
+ )
159
+ _write(
160
+ root / "auto-flush.md",
161
+ "\n".join(
162
+ ["# Auto Flush", ""]
163
+ + [
164
+ f"- #{item['id']} [{item.get('session_id','unknown')}] {item.get('created_at','')}: "
165
+ f"{item.get('summary','')}"
166
+ for item in flushes
167
+ ]
168
+ ),
169
+ )
170
+ _write(
171
+ root / "user-state.md",
172
+ "\n".join(
173
+ [
174
+ "# User State",
175
+ "",
176
+ f"- Current: {user_state['state_label']} ({user_state['confidence']})",
177
+ f"- Trust: {user_state['trust_score']}",
178
+ f"- Guidance: {user_state['guidance']}",
179
+ "",
180
+ "## Signals",
181
+ ]
182
+ + [f"- {key}: {value}" for key, value in user_state["signals"].items()]
183
+ + ["", "## History", ""]
184
+ + [f"- {item['created_at']} :: {item['state_label']} ({item['confidence']})" for item in user_history]
185
+ ),
186
+ )
187
+ _write(root / "cognitive.json", json.dumps(cognitive_stats, indent=2, sort_keys=True))
188
+ return f"Memory export written to {root}"
189
+
190
+
191
+ TOOLS = [
192
+ (handle_auto_flush_recent, "nexo_auto_flush_recent", "Show recent structured auto-flush records written before compaction."),
193
+ (handle_auto_flush_stats, "nexo_auto_flush_stats", "Stats for pre-compaction auto-flush activity."),
194
+ (handle_memory_backend_status, "nexo_memory_backend_status", "Show the active memory backend contract and registered backend list."),
195
+ (handle_memory_export, "nexo_memory_export", "Export a readable markdown snapshot of key NEXO memory layers."),
196
+ ]
@@ -0,0 +1,130 @@
1
+ """Outcome tracker plugin — close action -> expected result -> actual result loops."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from db import (
8
+ create_outcome,
9
+ get_outcome,
10
+ list_outcomes,
11
+ cancel_outcome,
12
+ evaluate_outcome,
13
+ list_outcome_pattern_candidates,
14
+ capture_outcome_pattern,
15
+ )
16
+
17
+
18
+ def _format_outcome_row(row: dict) -> str:
19
+ actual = row.get("actual_value")
20
+ actual_display = (
21
+ str(actual)
22
+ if actual is not None
23
+ else (row.get("actual_value_text") or "—")
24
+ )
25
+ return (
26
+ f"#{row['id']} [{row.get('status','pending')}] "
27
+ f"{row.get('action_type','custom')}:{row.get('action_id','—') or '—'} "
28
+ f"deadline={row.get('deadline','—')} "
29
+ f"expected={row.get('expected_result','')[:80]} "
30
+ f"actual={str(actual_display)[:80]}"
31
+ )
32
+
33
+
34
+ def handle_outcome_register(
35
+ action_type: str,
36
+ description: str,
37
+ expected_result: str,
38
+ metric_source: str = "manual",
39
+ metric_query: str = "",
40
+ baseline: float | None = None,
41
+ target: float | None = None,
42
+ target_op: str = "gte",
43
+ deadline: str = "",
44
+ action_id: str = "",
45
+ session_id: str = "",
46
+ notes: str = "",
47
+ ) -> str:
48
+ """Register an expected outcome for an action so NEXO can verify it later."""
49
+ result = create_outcome(
50
+ action_type=action_type,
51
+ description=description,
52
+ expected_result=expected_result,
53
+ metric_source=metric_source,
54
+ metric_query=metric_query,
55
+ baseline_value=baseline,
56
+ target_value=target,
57
+ target_operator=target_op,
58
+ deadline=deadline,
59
+ action_id=action_id,
60
+ session_id=session_id,
61
+ notes=notes,
62
+ )
63
+ if "error" in result:
64
+ return f"ERROR: {result['error']}"
65
+ return (
66
+ f"Outcome #{result['id']} registered [{result['status']}]. "
67
+ f"deadline={result['deadline']} metric_source={result['metric_source']}"
68
+ )
69
+
70
+
71
+ def handle_outcome_check(
72
+ id: int,
73
+ actual_value: float | None = None,
74
+ actual_value_text: str = "",
75
+ create_learning_on_miss: bool = True,
76
+ ) -> str:
77
+ """Check one outcome now and update its status using linked state or supplied evidence."""
78
+ result = evaluate_outcome(
79
+ int(id),
80
+ actual_value=actual_value,
81
+ actual_value_text=actual_value_text,
82
+ create_learning_on_miss=bool(create_learning_on_miss),
83
+ )
84
+ if "error" in result:
85
+ return f"ERROR: {result['error']}"
86
+ notes = result.get("notes") or ""
87
+ learning = f" learning_id={result.get('learning_id')}" if result.get("learning_id") else ""
88
+ return f"{_format_outcome_row(result)}{learning}\nnotes={notes[:300]}"
89
+
90
+
91
+ def handle_outcome_list(status: str = "", action_type: str = "", limit: int = 20) -> str:
92
+ """List outcomes by status and/or action type."""
93
+ rows = list_outcomes(status=status, action_type=action_type, limit=limit)
94
+ if not rows:
95
+ scope = f"status={status or 'any'} action_type={action_type or 'any'}"
96
+ return f"No outcomes found ({scope})."
97
+ header = f"OUTCOMES ({len(rows)})"
98
+ return "\n".join([header] + [f" {_format_outcome_row(row)}" for row in rows])
99
+
100
+
101
+ def handle_outcome_cancel(id: int, reason: str = "") -> str:
102
+ """Cancel a pending outcome so it no longer blocks reviews or release loops."""
103
+ result = cancel_outcome(int(id), reason=reason)
104
+ if "error" in result:
105
+ return f"ERROR: {result['error']}"
106
+ return f"Outcome #{result['id']} cancelled."
107
+
108
+
109
+ def handle_outcome_pattern_candidates(min_resolved: int = 3, limit: int = 10) -> str:
110
+ """List repeated resolved outcome patterns that are strong enough to become reusable knowledge."""
111
+ candidates = list_outcome_pattern_candidates(min_resolved=min_resolved, limit=limit)
112
+ return json.dumps({"ok": True, "candidates": candidates}, ensure_ascii=False, indent=2)
113
+
114
+
115
+ def handle_outcome_pattern_capture(pattern_key: str, target: str = "learning", category: str = "outcomes") -> str:
116
+ """Materialize one repeated outcome pattern into a reusable artifact (currently a learning)."""
117
+ result = capture_outcome_pattern(pattern_key=pattern_key, target=target, category=category)
118
+ if "error" in result:
119
+ return json.dumps({"ok": False, "error": result["error"]}, ensure_ascii=False, indent=2)
120
+ return json.dumps(result, ensure_ascii=False, indent=2)
121
+
122
+
123
+ TOOLS = [
124
+ (handle_outcome_register, "nexo_outcome_register", "Register an expected action outcome with metric source, deadline, and optional link to decision/followup/task."),
125
+ (handle_outcome_check, "nexo_outcome_check", "Check a tracked outcome now using linked state or supplied evidence; marks pending/met/missed and may create a learning on miss."),
126
+ (handle_outcome_list, "nexo_outcome_list", "List tracked outcomes filtered by status and/or action type."),
127
+ (handle_outcome_cancel, "nexo_outcome_cancel", "Cancel an outcome so it stops blocking pending/missed reviews."),
128
+ (handle_outcome_pattern_candidates, "nexo_outcome_pattern_candidates", "List repeated resolved outcome patterns from cortex-linked decisions that are consistent enough to become reusable knowledge."),
129
+ (handle_outcome_pattern_capture, "nexo_outcome_pattern_capture", "Capture one repeated outcome pattern as a reusable artifact (currently a learning) once the evidence is consistent."),
130
+ ]