nexo-brain 7.27.6 → 7.29.0

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 (41) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +5 -1
  3. package/package.json +1 -1
  4. package/src/automation_controls.py +7 -0
  5. package/src/automation_preferences.py +323 -0
  6. package/src/causal_graph.py +763 -0
  7. package/src/cli.py +120 -0
  8. package/src/cli_email.py +95 -0
  9. package/src/cognitive/_core.py +3 -0
  10. package/src/cognitive_control_observatory.py +2 -0
  11. package/src/db/_entities.py +98 -11
  12. package/src/db/_memory_v2.py +78 -0
  13. package/src/db/_schema.py +546 -0
  14. package/src/email_presentation.py +243 -0
  15. package/src/entity_live_profile.py +1073 -0
  16. package/src/failure_prevention.py +1052 -0
  17. package/src/knowledge_graph.py +46 -9
  18. package/src/local_context/usage_events.py +273 -8
  19. package/src/memory_executive.py +620 -0
  20. package/src/memory_utility.py +952 -0
  21. package/src/morning_briefing.py +281 -0
  22. package/src/plugin_loader.py +9 -5
  23. package/src/plugins/entities.py +84 -7
  24. package/src/plugins/entity_live_profile.py +101 -0
  25. package/src/plugins/failure_prevention.py +162 -0
  26. package/src/plugins/memory_export.py +55 -18
  27. package/src/plugins/protocol.py +37 -0
  28. package/src/plugins/semantic_layers.py +138 -0
  29. package/src/pre_answer_router.py +324 -22
  30. package/src/pre_answer_runtime.py +463 -18
  31. package/src/script_registry.py +15 -0
  32. package/src/scripts/nexo-morning-agent.py +118 -69
  33. package/src/scripts/nexo-send-reply.py +20 -25
  34. package/src/semantic_layers.py +1153 -0
  35. package/src/server.py +4 -2
  36. package/src/tools_sessions.py +88 -31
  37. package/src/tools_transcripts.py +38 -22
  38. package/src/user_state_model.py +971 -0
  39. package/templates/core-prompts/morning-agent-json-output.md +1 -1
  40. package/templates/core-prompts/morning-agent.md +5 -2
  41. package/tool-enforcement-map.json +230 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.27.6",
3
+ "version": "7.29.0",
4
4
  "description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
5
5
  "author": {
6
6
  "name": "NEXO Brain",
package/README.md CHANGED
@@ -18,7 +18,11 @@
18
18
 
19
19
  [Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
20
20
 
21
- Version `7.27.6` is the current packaged-runtime line. Patch release over v7.27.5 - operational memory continuity now persists promises as commitments, routes pre-answer questions through evidence-backed memory, and exposes observation-queue convergence in health checks.
21
+ Version `7.29.0` is the current packaged-runtime line. Minor release over v7.28.0 - the morning briefing now has structured content preferences, Desktop-facing briefing presentation state, and per-account email signature support.
22
+
23
+ Previously in `7.28.0`: minor release over v7.27.6 - the Brain memory architecture now links action authorship, reasons, operational state, entity profiles, failure prevention, semantic layers, and release-gated runtime memory benchmarks.
24
+
25
+ Previously in `7.27.6`: patch release over v7.27.5 - operational memory continuity now persists promises as commitments, routes pre-answer questions through evidence-backed memory, and exposes observation-queue convergence in health checks.
22
26
 
23
27
  Previously in `7.27.5`: patch release over v7.27.4 - Desktop onboarding asks for the user's name directly in Spanish with `¿Cómo te llamas?`, matching Desktop fallback copy.
24
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.27.6",
3
+ "version": "7.29.0",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
6
6
  "homepage": "https://nexo-brain.com",
@@ -672,10 +672,17 @@ def get_script_runtime_contract(name: str) -> dict[str, Any]:
672
672
  blocked_reason = str(recipient_status.get("reason") or "")
673
673
  blocked_reason_code = str(recipient_status.get("reason_code") or "")
674
674
 
675
+ try:
676
+ from automation_preferences import supports_automation_preferences
677
+ preferences_supported = supports_automation_preferences(clean_name)
678
+ except Exception:
679
+ preferences_supported = False
680
+
675
681
  return {
676
682
  "name": clean_name,
677
683
  "toggleable_core": is_toggleable_core_script(clean_name),
678
684
  "supports_extra_instructions": supports_operator_extra_instructions(clean_name),
685
+ "supports_automation_preferences": preferences_supported,
679
686
  "schedule_configurable": bool(schedule_state.get("schedule_configurable")),
680
687
  "schedule_type": str(schedule_state.get("schedule_type") or ""),
681
688
  "schedule_source": str(schedule_state.get("schedule_source") or ""),
@@ -0,0 +1,323 @@
1
+ """Structured operator preferences for product automations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import json
7
+ import unicodedata
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ AUTOMATION_PREFERENCES_METADATA_KEY = "automation_preferences"
13
+ SUPPORTED_AUTOMATIONS = {"morning-agent"}
14
+
15
+
16
+ MORNING_AGENT_SCHEMA: dict[str, Any] = {
17
+ "schema_version": 1,
18
+ "automation": "morning-agent",
19
+ "title": "Morning briefing content",
20
+ "groups": [
21
+ {
22
+ "id": "content",
23
+ "label": "Content",
24
+ "items": [
25
+ {"id": "priorities", "type": "boolean", "label": "Priorities", "default": True},
26
+ {"id": "agenda", "type": "boolean", "label": "Agenda", "default": True},
27
+ {"id": "reminders", "type": "boolean", "label": "Reminders and tasks", "default": True},
28
+ {"id": "followups", "type": "boolean", "label": "Follow-ups", "default": True},
29
+ {"id": "decisions", "type": "boolean", "label": "Recent decisions", "default": True},
30
+ {"id": "email_activity", "type": "boolean", "label": "Recent sent email", "default": True},
31
+ {"id": "blockers", "type": "boolean", "label": "Blockers and risks", "default": True},
32
+ {"id": "internal_refs", "type": "boolean", "label": "Internal references", "default": False},
33
+ {
34
+ "id": "news",
35
+ "type": "boolean",
36
+ "label": "News",
37
+ "default": False,
38
+ "disabled": True,
39
+ "disabled_reason": "No verified news source is connected yet.",
40
+ },
41
+ {
42
+ "id": "weather",
43
+ "type": "boolean",
44
+ "label": "Weather",
45
+ "default": False,
46
+ "disabled": True,
47
+ "disabled_reason": "No verified weather source is connected yet.",
48
+ },
49
+ ],
50
+ },
51
+ {
52
+ "id": "style",
53
+ "label": "Style",
54
+ "items": [
55
+ {
56
+ "id": "length",
57
+ "type": "choice",
58
+ "label": "Length",
59
+ "default": "normal",
60
+ "options": ["short", "normal", "detailed"],
61
+ },
62
+ {
63
+ "id": "tone",
64
+ "type": "choice",
65
+ "label": "Tone",
66
+ "default": "direct",
67
+ "options": ["direct", "warm", "executive", "personal"],
68
+ },
69
+ {
70
+ "id": "format",
71
+ "type": "choice",
72
+ "label": "Format",
73
+ "default": "sections",
74
+ "options": ["sections", "bullets", "narrative"],
75
+ },
76
+ ],
77
+ },
78
+ {
79
+ "id": "delivery",
80
+ "label": "Delivery",
81
+ "items": [
82
+ {
83
+ "id": "quiet_days",
84
+ "type": "choice",
85
+ "label": "Quiet days",
86
+ "default": "summary_if_anything_important",
87
+ "options": ["always_send", "summary_if_anything_important", "skip_if_empty"],
88
+ },
89
+ {
90
+ "id": "audience",
91
+ "type": "choice",
92
+ "label": "User type",
93
+ "default": "operator",
94
+ "options": ["operator", "founder", "student", "sales", "technical", "personal"],
95
+ },
96
+ ],
97
+ },
98
+ ],
99
+ }
100
+
101
+
102
+ def _schema_for(name: str) -> dict[str, Any] | None:
103
+ clean = normalize_automation_name(name)
104
+ if clean == "morning-agent":
105
+ return copy.deepcopy(MORNING_AGENT_SCHEMA)
106
+ return None
107
+
108
+
109
+ def normalize_automation_name(name: str) -> str:
110
+ return str(name or "").strip().lower().replace("_", "-")
111
+
112
+
113
+ def supports_automation_preferences(name: str) -> bool:
114
+ return normalize_automation_name(name) in SUPPORTED_AUTOMATIONS
115
+
116
+
117
+ def get_automation_preference_schema(name: str) -> dict[str, Any]:
118
+ schema = _schema_for(name)
119
+ if not schema:
120
+ return {
121
+ "schema_version": 1,
122
+ "automation": normalize_automation_name(name),
123
+ "title": "Automation content",
124
+ "groups": [],
125
+ }
126
+ return schema
127
+
128
+
129
+ def _iter_schema_items(schema: dict[str, Any]):
130
+ for group in list(schema.get("groups") or []):
131
+ for item in list(group.get("items") or []):
132
+ if isinstance(item, dict) and item.get("id"):
133
+ yield item
134
+
135
+
136
+ def default_automation_preferences(name: str) -> dict[str, Any]:
137
+ schema = get_automation_preference_schema(name)
138
+ values: dict[str, Any] = {}
139
+ for item in _iter_schema_items(schema):
140
+ values[str(item["id"])] = copy.deepcopy(item.get("default"))
141
+ return {
142
+ "schema_version": int(schema.get("schema_version") or 1),
143
+ "automation": normalize_automation_name(name),
144
+ "values": values,
145
+ }
146
+
147
+
148
+ def validate_automation_preferences(name: str, payload: dict[str, Any] | None) -> dict[str, Any]:
149
+ schema = get_automation_preference_schema(name)
150
+ defaults = default_automation_preferences(name)
151
+ source_values = {}
152
+ if isinstance(payload, dict):
153
+ if isinstance(payload.get("values"), dict):
154
+ source_values = payload.get("values") or {}
155
+ else:
156
+ source_values = payload
157
+ values = dict(defaults["values"])
158
+ warnings: list[str] = []
159
+ for item in _iter_schema_items(schema):
160
+ key = str(item["id"])
161
+ if key not in source_values:
162
+ continue
163
+ if item.get("disabled"):
164
+ values[key] = copy.deepcopy(item.get("default"))
165
+ warnings.append(f"{key}: disabled")
166
+ continue
167
+ kind = str(item.get("type") or "text")
168
+ raw_value = source_values.get(key)
169
+ if kind == "boolean":
170
+ values[key] = bool(raw_value)
171
+ elif kind == "choice":
172
+ options = [str(v) for v in list(item.get("options") or [])]
173
+ clean = str(raw_value or "").strip()
174
+ if clean in options:
175
+ values[key] = clean
176
+ else:
177
+ warnings.append(f"{key}: invalid choice")
178
+ elif kind == "number":
179
+ try:
180
+ values[key] = int(raw_value)
181
+ except Exception:
182
+ warnings.append(f"{key}: invalid number")
183
+ else:
184
+ values[key] = str(raw_value or "").strip()[:1000]
185
+ return {
186
+ "schema_version": int(schema.get("schema_version") or 1),
187
+ "automation": normalize_automation_name(name),
188
+ "values": values,
189
+ "warnings": warnings,
190
+ }
191
+
192
+
193
+ def _script_row_for(name_or_path: str) -> tuple[dict, dict] | tuple[None, None]:
194
+ from db import init_db
195
+ from db._personal_scripts import get_personal_script
196
+ from script_registry import resolve_script, sync_personal_scripts
197
+
198
+ init_db()
199
+ sync_personal_scripts()
200
+ script = get_personal_script(name_or_path, include_core=True) or resolve_script(name_or_path)
201
+ if not script and normalize_automation_name(name_or_path) == "morning-agent":
202
+ script_path = Path(__file__).resolve().parent / "scripts" / "nexo-morning-agent.py"
203
+ script = {
204
+ "name": "morning-agent",
205
+ "path": str(script_path),
206
+ "description": "Generate and send the operator's daily morning briefing email.",
207
+ "runtime": "python",
208
+ "core": True,
209
+ "metadata": {},
210
+ "origin": "core",
211
+ }
212
+ if not script:
213
+ return None, None
214
+ existing = get_personal_script(script.get("path", ""), include_core=True) or script
215
+ return script, existing
216
+
217
+
218
+ def get_automation_preferences(name_or_path: str) -> dict[str, Any]:
219
+ clean_name = normalize_automation_name(name_or_path)
220
+ script, existing = _script_row_for(name_or_path)
221
+ if script:
222
+ clean_name = normalize_automation_name(script.get("name") or clean_name)
223
+ metadata = (existing or {}).get("metadata") if isinstance((existing or {}).get("metadata"), dict) else {}
224
+ stored = metadata.get(AUTOMATION_PREFERENCES_METADATA_KEY) if isinstance(metadata, dict) else {}
225
+ validated = validate_automation_preferences(clean_name, stored if isinstance(stored, dict) else {})
226
+ return {
227
+ "ok": True,
228
+ "name": clean_name,
229
+ "schema": get_automation_preference_schema(clean_name),
230
+ "preferences": validated,
231
+ "supports_automation_preferences": supports_automation_preferences(clean_name),
232
+ }
233
+
234
+
235
+ def set_automation_preferences(name_or_path: str, payload: dict[str, Any]) -> dict[str, Any]:
236
+ from db._personal_scripts import upsert_personal_script
237
+
238
+ script, existing = _script_row_for(name_or_path)
239
+ if not script:
240
+ return {"ok": False, "error": f"Automation not found: {name_or_path}"}
241
+ clean_name = normalize_automation_name(script.get("name") or name_or_path)
242
+ if not supports_automation_preferences(clean_name):
243
+ return {"ok": False, "error": "This automation does not support structured preferences."}
244
+ validated = validate_automation_preferences(clean_name, payload)
245
+ metadata = dict((existing or script).get("metadata") or {})
246
+ metadata[AUTOMATION_PREFERENCES_METADATA_KEY] = {
247
+ "schema_version": validated["schema_version"],
248
+ "values": validated["values"],
249
+ }
250
+ script_origin = "core" if (bool(script.get("core")) or str(script.get("origin") or "") == "core") else "user"
251
+ upsert_personal_script(
252
+ name=script.get("name", clean_name),
253
+ path=script.get("path", ""),
254
+ description=script.get("description", ""),
255
+ runtime=script.get("runtime", "unknown"),
256
+ metadata=metadata,
257
+ created_by="nexo-core" if script_origin == "core" else "manual",
258
+ source="core-toggle" if script_origin == "core" else "filesystem",
259
+ origin=script_origin,
260
+ enabled=bool((existing or script).get("enabled", True)),
261
+ has_inline_metadata=bool(script.get("metadata")),
262
+ )
263
+ return {
264
+ "ok": True,
265
+ "name": clean_name,
266
+ "preferences": validated,
267
+ "supports_automation_preferences": True,
268
+ }
269
+
270
+
271
+ def search_automation_preference_schema(name: str, query: str) -> list[dict[str, Any]]:
272
+ clean_query = _fold_text(query)
273
+ if not clean_query:
274
+ return []
275
+ matches: list[dict[str, Any]] = []
276
+ for group in list(get_automation_preference_schema(name).get("groups") or []):
277
+ for item in list(group.get("items") or []):
278
+ text = " ".join([
279
+ str(item.get("id") or ""),
280
+ str(item.get("label") or ""),
281
+ str(item.get("disabled_reason") or ""),
282
+ str(group.get("label") or ""),
283
+ ])
284
+ if clean_query in _fold_text(text):
285
+ matches.append({"group": group.get("id"), **item})
286
+ return matches
287
+
288
+
289
+ def _fold_text(value: str) -> str:
290
+ normalized = unicodedata.normalize("NFKD", str(value or ""))
291
+ asciiish = "".join(ch for ch in normalized if not unicodedata.combining(ch))
292
+ return asciiish.casefold()
293
+
294
+
295
+ def format_automation_preferences_prompt_block(name_or_path: str) -> str:
296
+ result = get_automation_preferences(name_or_path)
297
+ if not result.get("supports_automation_preferences"):
298
+ return ""
299
+ prefs = result.get("preferences") or {}
300
+ values = prefs.get("values") if isinstance(prefs, dict) else {}
301
+ if not isinstance(values, dict):
302
+ values = {}
303
+ compact = json.dumps(values, ensure_ascii=False, sort_keys=True)
304
+ return (
305
+ "\n== STRUCTURED CONTENT PREFERENCES FOR THIS AUTOMATION ==\n"
306
+ f"{compact}\n"
307
+ "Use these preferences to decide what to include, omit, and emphasize. "
308
+ "Disabled/unavailable data sources must not be invented.\n"
309
+ )
310
+
311
+
312
+ __all__ = [
313
+ "AUTOMATION_PREFERENCES_METADATA_KEY",
314
+ "default_automation_preferences",
315
+ "format_automation_preferences_prompt_block",
316
+ "get_automation_preference_schema",
317
+ "get_automation_preferences",
318
+ "normalize_automation_name",
319
+ "search_automation_preference_schema",
320
+ "set_automation_preferences",
321
+ "supports_automation_preferences",
322
+ "validate_automation_preferences",
323
+ ]