nexo-brain 5.4.1 → 5.4.4

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "5.4.1",
3
+ "version": "5.4.4",
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,7 @@
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 `5.4.1` is the current packaged-runtime line: hook hygiene fix the PostToolUse `capture-session.sh` hook had been reading a nonexistent env var since 2026-04-12, silently writing `"tool":"unknown"` to the Sensory Register for 48 hours. v5.4.1 parses the tool name from stdin JSON, removes the filter that was hiding `Bash`, and purges pre-fix entries from the buffer on update.
21
+ Version `5.4.4` is the current packaged-runtime line: test isolation for tree_hygiene module + fake venv to prevent CI timeout completes the publish workflow fix from v5.4.3.
22
22
 
23
23
  Previously in `5.4.0`: runtime event bus at `~/.nexo/runtime/events.ndjson`, `nexo notify`, `nexo health --json`, `nexo logs --tail --json`, and a safe flat→nested migration for `calibration.json`.
24
24
 
@@ -630,20 +630,21 @@ PreCompact hook saves full checkpoint if conversation is compressed
630
630
 
631
631
  PostCompact hook re-injects Core Memory Block → session continues seamlessly
632
632
 
633
- Stop hook triggers mandatory post-mortem:
634
- - Self-critique: 5 questions about what could be better
635
- - Session buffer: structured entry for the reflection engine
636
- - Followups: anything promised gets scheduled
637
- - Proactive seeds: what can the next session do without being asked?
633
+ Stop hook refreshes the diary draft and approves immediately:
634
+ - Latest changes and decisions stay attached to the active session
635
+ - Session buffer keeps structured tool activity for downstream processing
636
+ - Followups and closing synthesis happen inline when the agent detects real closing intent
637
+ - No mid-conversation blocking from the hook itself
638
638
 
639
- Reflection engine processes buffer (after 3+ sessions)
639
+ Nocturnal post-mortem consolidator processes the buffer mechanically
640
640
 
641
641
  Nocturnal processes: decay, consolidation, self-audit, dreaming
642
642
  ```
643
643
 
644
644
  ### Reflection Engine
645
645
 
646
- After 3+ sessions accumulate, the stop hook triggers `nexo-reflection.py`:
646
+ NEXO still ships `nexo-reflection.py` as a standalone analyzer for `session_buffer.jsonl`.
647
+ It is not currently auto-triggered by the stop hook:
647
648
  - Extracts recurring tasks, error patterns, mood trends
648
649
  - Updates `user_model.json` with observed behavior
649
650
  - No LLM required — runs as pure Python
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "5.4.1",
3
+ "version": "5.4.4",
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",
@@ -2115,6 +2115,7 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
2115
2115
  nexo_home=dest,
2116
2116
  runtime_root=dest,
2117
2117
  preferences=normalized_preferences,
2118
+ auto_install_missing_claude=True,
2118
2119
  )
2119
2120
  if client_sync_result.get("ok"):
2120
2121
  actions.append("client-sync")
@@ -24,6 +24,7 @@ try:
24
24
  from client_preferences import (
25
25
  BACKEND_NONE,
26
26
  INTERACTIVE_CLIENT_KEYS,
27
+ detect_installed_clients,
27
28
  normalize_backend_key,
28
29
  normalize_client_key,
29
30
  normalize_client_preferences,
@@ -68,6 +69,16 @@ except Exception:
68
69
  }
69
70
  return dict(defaults.get(client, {}))
70
71
 
72
+ def detect_installed_clients(user_home: str | os.PathLike[str] | None = None) -> dict[str, dict]:
73
+ return {
74
+ "claude_code": {"installed": False, "path": "", "detected_by": "missing"},
75
+ "codex": {"installed": False, "path": "", "detected_by": "missing"},
76
+ "claude_desktop": {"installed": False, "path": "", "detected_by": "missing"},
77
+ }
78
+
79
+
80
+ CLAUDE_CODE_NPM_PACKAGE = "@anthropic-ai/claude-code"
81
+
71
82
 
72
83
 
73
84
  def _user_home() -> Path:
@@ -133,6 +144,108 @@ def _resolve_python(nexo_home: Path, explicit: str = "") -> str:
133
144
  return explicit or sys.executable
134
145
 
135
146
 
147
+ def _cli_install_env(user_home: Path | None = None) -> dict[str, str]:
148
+ env = os.environ.copy()
149
+ if user_home is not None:
150
+ env["HOME"] = str(user_home)
151
+ return env
152
+
153
+
154
+ def _installed_client_path(client_key: str, *, user_home: Path | None = None) -> str:
155
+ info = detect_installed_clients(user_home=user_home).get(client_key, {})
156
+ if info.get("installed"):
157
+ return str(info.get("path") or "")
158
+ return ""
159
+
160
+
161
+ def ensure_claude_code_installed(*, user_home: str | os.PathLike[str] | None = None) -> dict:
162
+ home_path = Path(user_home).expanduser() if user_home else _user_home()
163
+ existing = _installed_client_path("claude_code", user_home=home_path)
164
+ if existing:
165
+ return {
166
+ "ok": True,
167
+ "client": "claude_code",
168
+ "installed": True,
169
+ "changed": False,
170
+ "action": "already_installed",
171
+ "path": existing,
172
+ "attempts": [],
173
+ }
174
+
175
+ attempts: list[str] = []
176
+ env = _cli_install_env(home_path)
177
+
178
+ try:
179
+ probe = subprocess.run(
180
+ ["npx", "-y", CLAUDE_CODE_NPM_PACKAGE, "--version"],
181
+ capture_output=True,
182
+ text=True,
183
+ timeout=60,
184
+ env=env,
185
+ )
186
+ if probe.returncode != 0:
187
+ attempts.append((probe.stderr or probe.stdout or "npx probe failed").strip())
188
+ except Exception as exc:
189
+ attempts.append(f"npx probe failed: {exc}")
190
+
191
+ installed_after_probe = _installed_client_path("claude_code", user_home=home_path)
192
+ if installed_after_probe:
193
+ return {
194
+ "ok": True,
195
+ "client": "claude_code",
196
+ "installed": True,
197
+ "changed": True,
198
+ "action": "installed_via_npx",
199
+ "path": installed_after_probe,
200
+ "attempts": attempts,
201
+ }
202
+
203
+ install_cmd = (
204
+ ["sudo", "npm", "install", "-g", CLAUDE_CODE_NPM_PACKAGE]
205
+ if sys.platform == "linux"
206
+ else ["npm", "install", "-g", CLAUDE_CODE_NPM_PACKAGE]
207
+ )
208
+ install_error = ""
209
+ try:
210
+ install = subprocess.run(
211
+ install_cmd,
212
+ capture_output=True,
213
+ text=True,
214
+ timeout=180,
215
+ env=env,
216
+ )
217
+ if install.returncode != 0:
218
+ install_error = (install.stderr or install.stdout or "npm install failed").strip()
219
+ attempts.append(install_error)
220
+ except Exception as exc:
221
+ install_error = f"npm install failed: {exc}"
222
+ attempts.append(install_error)
223
+
224
+ installed_path = _installed_client_path("claude_code", user_home=home_path)
225
+ if installed_path:
226
+ return {
227
+ "ok": True,
228
+ "client": "claude_code",
229
+ "installed": True,
230
+ "changed": True,
231
+ "action": "installed",
232
+ "path": installed_path,
233
+ "attempts": attempts,
234
+ }
235
+
236
+ error = install_error or "Claude Code install did not produce a `claude` binary in PATH"
237
+ return {
238
+ "ok": False,
239
+ "client": "claude_code",
240
+ "installed": False,
241
+ "changed": False,
242
+ "action": "failed",
243
+ "path": "",
244
+ "attempts": attempts,
245
+ "error": error,
246
+ }
247
+
248
+
136
249
  def build_server_config(
137
250
  *,
138
251
  nexo_home: str | os.PathLike[str] | None = None,
@@ -856,6 +969,7 @@ def sync_all_clients(
856
969
  user_home: str | os.PathLike[str] | None = None,
857
970
  enabled_clients: list[str] | tuple[str, ...] | set[str] | None = None,
858
971
  preferences: dict | None = None,
972
+ auto_install_missing_claude: bool = False,
859
973
  ) -> dict:
860
974
  if enabled_clients is None:
861
975
  if preferences is None:
@@ -877,6 +991,10 @@ def sync_all_clients(
877
991
  if not enabled_set:
878
992
  enabled_set = {"claude_code"}
879
993
 
994
+ install_results: dict[str, dict] = {}
995
+ if auto_install_missing_claude and "claude_code" in enabled_set:
996
+ install_results["claude_code"] = ensure_claude_code_installed(user_home=user_home)
997
+
880
998
  def _safe(label: str, fn) -> dict:
881
999
  if label not in enabled_set:
882
1000
  return {
@@ -902,7 +1020,10 @@ def sync_all_clients(
902
1020
  "claude_desktop": _safe("claude_desktop", sync_claude_desktop),
903
1021
  "codex": _safe("codex", sync_codex),
904
1022
  }
905
- ok = all(item.get("ok") or item.get("skipped") for item in results.values())
1023
+ ok = (
1024
+ all(item.get("ok") or item.get("skipped") for item in results.values())
1025
+ and all(item.get("ok") for item in install_results.values())
1026
+ )
906
1027
  return {
907
1028
  "ok": ok,
908
1029
  "nexo_home": str(Path(nexo_home).expanduser() if nexo_home else _default_nexo_home()),
@@ -911,6 +1032,7 @@ def sync_all_clients(
911
1032
  runtime_root,
912
1033
  )),
913
1034
  "enabled_clients": sorted(enabled_set),
1035
+ "install_results": install_results,
914
1036
  "clients": results,
915
1037
  }
916
1038
 
@@ -1773,7 +1773,7 @@ def check_personal_script_registry(fix: bool = False) -> DoctorCheck:
1773
1773
  )
1774
1774
 
1775
1775
 
1776
- def check_client_backend_preferences() -> DoctorCheck:
1776
+ def check_client_backend_preferences(fix: bool = False) -> DoctorCheck:
1777
1777
  schedule = {}
1778
1778
  try:
1779
1779
  if SCHEDULE_FILE.is_file():
@@ -1828,7 +1828,28 @@ def check_client_backend_preferences() -> DoctorCheck:
1828
1828
  repair_plan.append(f"Install {automation_backend} or disable automation in schedule.json")
1829
1829
 
1830
1830
  if not repair_plan and status != "healthy":
1831
- repair_plan.append("Run `nexo update` or `nexo clients sync` after installing the selected client/backend")
1831
+ repair_plan.append(
1832
+ "Run `nexo doctor --tier runtime --fix` or `nexo update` so the selected client/backend is installed and re-synced where possible"
1833
+ )
1834
+
1835
+ if fix and status != "healthy":
1836
+ try:
1837
+ from client_sync import sync_all_clients
1838
+
1839
+ sync_all_clients(
1840
+ nexo_home=NEXO_HOME,
1841
+ runtime_root=NEXO_CODE,
1842
+ user_home=Path.home(),
1843
+ preferences=prefs,
1844
+ auto_install_missing_claude=True,
1845
+ )
1846
+ except Exception:
1847
+ pass
1848
+ post = check_client_backend_preferences(fix=False)
1849
+ if post.status == "healthy":
1850
+ post.fixed = True
1851
+ post.summary += " (fixed)"
1852
+ return post
1832
1853
 
1833
1854
  def _profile_label(client_key: str, profile: dict[str, str]) -> str:
1834
1855
  bits = [client_key]
@@ -3151,7 +3172,7 @@ def run_runtime_checks(fix: bool = False) -> list[DoctorCheck]:
3151
3172
  safe_check(check_watchdog_status),
3152
3173
  safe_check(check_stale_sessions),
3153
3174
  safe_check(check_cron_freshness),
3154
- safe_check(check_client_backend_preferences),
3175
+ safe_check(check_client_backend_preferences, fix=fix),
3155
3176
  safe_check(check_client_bootstrap_parity, fix=fix),
3156
3177
  safe_check(check_codex_session_parity),
3157
3178
  safe_check(check_codex_conditioned_file_discipline),
@@ -3,6 +3,7 @@
3
3
  import datetime
4
4
  import json
5
5
  import time
6
+ from pathlib import Path
6
7
  from db import (
7
8
  log_decision, update_decision_outcome, search_decisions,
8
9
  write_session_diary, read_session_diary,
@@ -10,6 +11,23 @@ from db import (
10
11
  recall, get_db, set_linked_outcomes_met,
11
12
  )
12
13
 
14
+ REPO_ROOT = Path(__file__).resolve().parents[2]
15
+ _REPO_ABS_PREFIX = str(REPO_ROOT) + "/"
16
+ _REPO_IGNORED_TOP_LEVEL = {
17
+ ".git",
18
+ ".venv",
19
+ ".pytest_cache",
20
+ ".ruff_cache",
21
+ "__pycache__",
22
+ }
23
+ try:
24
+ _REPO_TOP_LEVEL_ENTRIES = {
25
+ path.name for path in REPO_ROOT.iterdir()
26
+ if path.name not in _REPO_IGNORED_TOP_LEVEL
27
+ }
28
+ except OSError:
29
+ _REPO_TOP_LEVEL_ENTRIES = set()
30
+
13
31
 
14
32
  def _cognitive_ingest_safe(content, source_type, source_id="", source_title="", domain=""):
15
33
  """Ingest to cognitive STM. Silently fails if cognitive engine unavailable."""
@@ -20,6 +38,37 @@ def _cognitive_ingest_safe(content, source_type, source_id="", source_title="",
20
38
  pass # Cognitive is optional — never block operational writes
21
39
 
22
40
 
41
+ def _iter_change_paths(files: str) -> list[str]:
42
+ parts = []
43
+ for chunk in str(files or "").replace("\n", ",").split(","):
44
+ item = chunk.strip()
45
+ if item:
46
+ parts.append(item)
47
+ return parts
48
+
49
+
50
+ def _change_requires_git_commit_ref(files: str) -> bool:
51
+ for item in _iter_change_paths(files):
52
+ normalized = item.replace("\\", "/").strip()
53
+ while normalized.startswith("./"):
54
+ normalized = normalized[2:]
55
+ if normalized.startswith(_REPO_ABS_PREFIX):
56
+ return True
57
+ top_level = normalized.split("/", 1)[0]
58
+ if top_level in _REPO_TOP_LEVEL_ENTRIES:
59
+ return True
60
+ return False
61
+
62
+
63
+ def _format_change_count(count: int) -> str:
64
+ return f"{count} cambio" if count == 1 else f"{count} cambios"
65
+
66
+
67
+ def _recent_change_phrase(count: int) -> str:
68
+ base = _format_change_count(count)
69
+ return f"{base} reciente" if count == 1 else f"{base} recientes"
70
+
71
+
23
72
  def handle_decision_log(domain: str, decision: str, alternatives: str = '',
24
73
  based_on: str = '', confidence: str = 'medium',
25
74
  context_ref: str = '', session_id: str = '',
@@ -226,23 +275,35 @@ def handle_session_diary_write(decisions: str, summary: str,
226
275
  # Episodic memory audit — warn about gaps
227
276
  warnings = []
228
277
  conn = __import__('db').get_db()
229
- orphan_changes = conn.execute(
230
- "SELECT COUNT(*) FROM change_log WHERE (commit_ref IS NULL OR commit_ref = '')"
231
- ).fetchone()[0]
232
- recent_orphan_changes = conn.execute(
233
- """SELECT COUNT(*) FROM change_log
234
- WHERE (commit_ref IS NULL OR commit_ref = '')
235
- AND created_at >= datetime('now', '-7 days')"""
236
- ).fetchone()[0]
237
- if orphan_changes > 0:
238
- if recent_orphan_changes > 0 and recent_orphan_changes != orphan_changes:
278
+ orphan_change_rows = conn.execute(
279
+ """SELECT files, created_at FROM change_log
280
+ WHERE (commit_ref IS NULL OR commit_ref = '')"""
281
+ ).fetchall()
282
+ repo_orphan_changes = 0
283
+ recent_repo_orphan_changes = 0
284
+ recent_cutoff = conn.execute("SELECT datetime('now', '-7 days')").fetchone()[0]
285
+ for row in orphan_change_rows:
286
+ files = row["files"] if hasattr(row, "keys") else row[0]
287
+ created_at = row["created_at"] if hasattr(row, "keys") else row[1]
288
+ if not _change_requires_git_commit_ref(files):
289
+ continue
290
+ repo_orphan_changes += 1
291
+ if created_at >= recent_cutoff:
292
+ recent_repo_orphan_changes += 1
293
+ if repo_orphan_changes > 0:
294
+ if recent_repo_orphan_changes > 0 and recent_repo_orphan_changes != repo_orphan_changes:
239
295
  warnings.append(
240
- f"{recent_orphan_changes} changes recientes sin commit_ref ({orphan_changes} históricas total)"
296
+ f"{_recent_change_phrase(recent_repo_orphan_changes)} de repo sin commit_ref "
297
+ f"({_format_change_count(repo_orphan_changes)} de repo total)"
298
+ )
299
+ elif recent_repo_orphan_changes > 0:
300
+ warnings.append(
301
+ f"{_recent_change_phrase(recent_repo_orphan_changes)} de repo sin commit_ref"
241
302
  )
242
- elif recent_orphan_changes > 0:
243
- warnings.append(f"{recent_orphan_changes} changes recientes sin commit_ref")
244
303
  else:
245
- warnings.append(f"{orphan_changes} changes históricas sin commit_ref")
304
+ warnings.append(
305
+ f"{_format_change_count(repo_orphan_changes)} históricos de repo sin commit_ref"
306
+ )
246
307
  orphan_decisions = conn.execute(
247
308
  "SELECT COUNT(*) FROM decisions WHERE (outcome IS NULL OR outcome = '') AND created_at < datetime('now', '-7 days')"
248
309
  ).fetchone()[0]
@@ -342,7 +403,16 @@ def handle_change_log(files: str, what_changed: str, why: str,
342
403
  pass
343
404
  msg = f"Change #{change_id} recorded: {files[:60]} — {what_changed[:60]}"
344
405
  if not commit_ref:
345
- msg += f"\n⚠ NO COMMIT. Use nexo_change_commit({change_id}, 'hash') after push, or 'server-direct' if it was a direct server edit."
406
+ if _change_requires_git_commit_ref(files):
407
+ msg += (
408
+ f"\n⚠ NO COMMIT. Use nexo_change_commit({change_id}, 'hash') after push."
409
+ )
410
+ else:
411
+ msg += (
412
+ f"\n⚠ NO COMMIT GIT. If this was a local/server-side change, link a marker "
413
+ f"with nexo_change_commit({change_id}, 'server-direct') or "
414
+ f"'local-uncommitted'."
415
+ )
346
416
  return msg
347
417
 
348
418
 
@@ -479,6 +479,7 @@ def _sync_packaged_clients() -> tuple[bool, str | None]:
479
479
  runtime_root=NEXO_HOME,
480
480
  operator_name=os.environ.get("NEXO_NAME", ""),
481
481
  preferences=preferences,
482
+ auto_install_missing_claude=True,
482
483
  )
483
484
  except Exception as e:
484
485
  return False, f"client sync failed: {e}"
@@ -87,6 +87,41 @@ def log(msg: str):
87
87
  f.write(line + "\n")
88
88
 
89
89
 
90
+ def _render_sensory_event_content(event: dict) -> str:
91
+ source = event.get("source", "")
92
+ if source == "hook-fallback":
93
+ task_str = " ".join(event.get("tasks", []))
94
+ if len(task_str) < 50 or "," in task_str:
95
+ return ""
96
+
97
+ parts = []
98
+ tool_name = str(event.get("tool") or "").strip()
99
+ if source == "hook" and tool_name:
100
+ parts.append(f"Tool activity via hook: {tool_name}")
101
+
102
+ for key, label in [("tasks", "Tasks"), ("decisions", "Decisions"),
103
+ ("errors_resolved", "Errors"), ("user_patterns", "the user")]:
104
+ val = event.get(key, [])
105
+ if val:
106
+ parts.append(f"{label}: {'; '.join(str(v) for v in val[:3])}")
107
+
108
+ critique = event.get("self_critique", "")
109
+ if critique and "hook-fallback" not in critique:
110
+ parts.append(f"Self-critique: {critique[:200]}")
111
+
112
+ return " | ".join(parts)
113
+
114
+
115
+ def _rewrite_session_buffer(lines: list[str]) -> None:
116
+ payload = "\n".join(lines)
117
+ if payload:
118
+ payload += "\n"
119
+
120
+ tmp_path = SESSION_BUFFER.with_suffix(SESSION_BUFFER.suffix + ".tmp")
121
+ tmp_path.write_text(payload)
122
+ tmp_path.replace(SESSION_BUFFER)
123
+
124
+
90
125
  # ─── Stage 1: Data Collection (Pure Python) ─────────────────────────────────
91
126
 
92
127
  def collect_data() -> dict:
@@ -257,28 +292,29 @@ def process_sensory_register():
257
292
  log(" No session_buffer.jsonl found, skipping")
258
293
  return
259
294
 
260
- today_events = []
295
+ pending_events = []
296
+ retained_lines = []
261
297
  try:
262
298
  with open(SESSION_BUFFER) as f:
263
299
  for line in f:
264
- line = line.strip()
300
+ raw_line = line.rstrip("\n")
301
+ line = raw_line.strip()
265
302
  if not line:
266
303
  continue
267
304
  try:
268
305
  event = json.loads(line)
269
- if event.get("ts", "").startswith(TODAY_STR):
270
- today_events.append(event)
306
+ pending_events.append((event, line))
271
307
  except json.JSONDecodeError:
272
- continue
308
+ retained_lines.append(raw_line)
273
309
  except Exception as e:
274
310
  log(f" Error reading session_buffer: {e}")
275
311
  return
276
312
 
277
- if not today_events:
278
- log(" No events from today")
313
+ if not pending_events:
314
+ log(" No pending events")
279
315
  return
280
316
 
281
- log(f" Found {len(today_events)} events")
317
+ log(f" Found {len(pending_events)} pending events")
282
318
 
283
319
  try:
284
320
  import cognitive
@@ -287,30 +323,16 @@ def process_sensory_register():
287
323
  return
288
324
 
289
325
  ingested = 0
290
- for event in today_events:
291
- source = event.get("source", "")
292
- if source == "hook-fallback":
293
- task_str = " ".join(event.get("tasks", []))
294
- if len(task_str) < 50 or "," in task_str:
295
- continue
296
-
297
- parts = []
298
- for key, label in [("tasks", "Tasks"), ("decisions", "Decisions"),
299
- ("errors_resolved", "Errors"), ("user_patterns", "the user")]:
300
- val = event.get(key, [])
301
- if val:
302
- parts.append(f"{label}: {'; '.join(str(v) for v in val[:3])}")
303
-
304
- critique = event.get("self_critique", "")
305
- if critique and "hook-fallback" not in critique:
306
- parts.append(f"Self-critique: {critique[:200]}")
307
-
308
- content = " | ".join(parts)
326
+ processed_events = 0
327
+ kept_events = 0
328
+ for event, raw_line in pending_events:
329
+ content = _render_sensory_event_content(event)
309
330
  if not content or len(content) < 20:
331
+ retained_lines.append(raw_line)
332
+ kept_events += 1
310
333
  continue
311
334
 
312
335
  try:
313
- vec = cognitive.embed(content)
314
336
  domain = ""
315
337
  lower = content.lower()
316
338
  # Add your project keywords for domain detection
@@ -324,10 +346,20 @@ def process_sensory_register():
324
346
  domain=domain, created_at=event.get("ts", "")
325
347
  )
326
348
  ingested += 1
349
+ processed_events += 1
327
350
  except Exception as e:
328
351
  log(f" Error embedding: {e}")
352
+ retained_lines.append(raw_line)
353
+
354
+ try:
355
+ _rewrite_session_buffer(retained_lines)
356
+ except Exception as e:
357
+ log(f" Error rewriting session_buffer: {e}")
329
358
 
330
- log(f" Ingested {ingested} sensory events into STM")
359
+ log(
360
+ f" Ingested {ingested} sensory events into STM "
361
+ f"(processed: {processed_events}, retained: {len(retained_lines)} incl kept={kept_events})"
362
+ )
331
363
 
332
364
 
333
365
  def analyze_force_events():
@@ -2,8 +2,8 @@
2
2
  """
3
3
  NEXO Reflection Engine — Processes session_buffer.jsonl entries.
4
4
 
5
- Triggered by the stop hook when >=3 sessions have accumulated and
6
- the last reflection was >4 hours ago.
5
+ The runtime still ships this as a standalone analyzer, but it is not
6
+ currently auto-triggered by the stop hook.
7
7
 
8
8
  What it does:
9
9
  1. Reads all entries from session_buffer.jsonl