nexo-brain 7.9.33 → 7.9.34

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": "7.9.33",
3
+ "version": "7.9.34",
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,9 @@
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.9.33` is the current packaged-runtime line. Patch release over `7.9.32`: adds ``usedforsecurity=False`` to the SHA-1 call that derives a filesystem-safe checkpoint filename from the email's Message-ID, so Bandit's B324 audit no longer fails the publish workflow on a non-security usage. The ``v7.9.32`` git tag is preserved for traceability but no npm release ever shipped for it; ``nexo-brain@7.9.33`` is the first release that carries the 7.9.32 email-recovery checkpoints.
21
+ Version `7.9.34` is the current packaged-runtime line. Patch release with two fixes. First, the email monitor's header parser was dropping any email whose RFC822 headers came back as ``email.header.Header`` instances (Q-encoded utf-8 / quoted-printable, common when sender names or subjects contain non-ASCII). ``msg.get("Message-ID").strip()`` raised ``TypeError: 'Header' object is not subscriptable``, the exception was swallowed at DEBUG, and the email was discarded silently operators only noticed when Nero stopped replying. Every ``msg.get(...)`` now goes through ``_decode_header`` (which decodes Q-encoding AND coerces to ``str``), and the failure log is lifted from DEBUG to WARNING so a future regression cannot drop emails silently. Second, the PreToolUse Guardian gate emitted JSON ``permissionDecision: deny`` on a hard-mode block but exit 0 — terminal Claude Code occasionally proceeded with the next tool anyway because the JSON deny channel was being dropped or out-of-order delivered mid-tool-loop. Hard blocks now also write the structured Guardian reason to stderr and exit 2, the documented PreToolUse blocking exit. Belt-and-suspenders enforcement: the model receives the same Guardian reason through both channels and self-corrects instead of blindly retrying.
22
+
23
+ Previously in `7.9.33`: adds ``usedforsecurity=False`` to the SHA-1 call that derives a filesystem-safe checkpoint filename from the email's Message-ID, so Bandit's B324 audit no longer fails the publish workflow on a non-security usage. The ``v7.9.32`` git tag is preserved for traceability but no npm release ever shipped for it; ``nexo-brain@7.9.33`` is the first release that carries the 7.9.32 email-recovery checkpoints.
22
24
 
23
25
  Previously in `7.9.32`: hardens the email monitor's recovery so emails that fall between Brain releases never end up in a permanent limbo. The periodic ``_recover_unreplied_processed`` sweep now looks back 7 days (was 24h), and every failed worker run persists a per-email checkpoint at ``~/.nexo/nexo-email/checkpoints/`` capturing files touched, last assistant narration, and error. Retry attempts inject that context into the next prompt so a long task (drafting a presentation, multi-step analysis) continues from where the previous attempt died instead of restarting from scratch. Stale checkpoints are pruned automatically after 7 days. 15 new unit tests cover the helpers.
24
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.9.33",
3
+ "version": "7.9.34",
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",
@@ -143,8 +143,20 @@ def main() -> int:
143
143
  "permissionDecisionReason": "Guardian gate blocked this tool call.",
144
144
  },
145
145
  }))
146
+ # 7.9.34 hardening: terminal Claude Code sometimes ignored the
147
+ # JSON deny channel mid-tool-loop and ran the next tool anyway.
148
+ # Belt-and-suspenders — also write the reason to stderr and exit
149
+ # with code 2, the documented blocking exit for PreToolUse. The
150
+ # JSON response stays the primary contract, but exit 2 forces
151
+ # the block at the process-exit layer and surfaces the reason to
152
+ # the model so it self-corrects instead of retrying blindly.
153
+ try:
154
+ sys.stderr.write(reason + "\n")
155
+ sys.stderr.flush()
156
+ except Exception:
157
+ pass
146
158
  summary = "blocked"
147
- exit_code = 0 # JSON response is the canonical path; non-zero is redundant
159
+ exit_code = 2
148
160
 
149
161
  elif isinstance(result, dict) and result.get("skipped"):
150
162
  summary = f"skipped:{result.get('reason', '')[:40]}"
@@ -1037,7 +1037,15 @@ def _decode_header(raw):
1037
1037
  def _parse_email_headers(raw_bytes):
1038
1038
  """Parse minimal headers from RFC822 header bytes. Returns dict with
1039
1039
  message_id, from_addr, from_name, subject, received_at, thread_id,
1040
- in_reply_to. Empty strings on failure."""
1040
+ in_reply_to. Empty strings on failure.
1041
+
1042
+ Q-encoded headers (utf-8 / quoted-printable) come back as
1043
+ `email.header.Header` instances rather than plain strings. Plain
1044
+ `str(Header)` returns the still-encoded `=?utf-8?q?...?=` form, and
1045
+ `Header` itself does not support `.strip()` / `in` — both of which
1046
+ used to drop the email silently in production. Every `msg.get(...)`
1047
+ therefore goes through `_decode_header`, which decodes Q-encoding
1048
+ AND coerces to a real `str`."""
1041
1049
  try:
1042
1050
  import email as _email
1043
1051
  msg = _email.message_from_bytes(raw_bytes)
@@ -1047,17 +1055,21 @@ def _parse_email_headers(raw_bytes):
1047
1055
  if "<" in from_raw and ">" in from_raw:
1048
1056
  name = from_raw.split("<")[0].strip().strip('"')
1049
1057
  addr = from_raw.split("<")[1].split(">")[0].strip()
1058
+ references_raw = _decode_header(msg.get("References", ""))
1059
+ in_reply_to_raw = _decode_header(msg.get("In-Reply-To", ""))
1060
+ thread_seed = (references_raw or in_reply_to_raw).strip()
1061
+ thread_id = thread_seed.split()[-1] if thread_seed else ""
1050
1062
  return {
1051
- "message_id": (msg.get("Message-ID") or "").strip(),
1063
+ "message_id": _decode_header(msg.get("Message-ID", "")).strip(),
1052
1064
  "from_addr": addr.strip().lower(),
1053
1065
  "from_name": name,
1054
1066
  "subject": _decode_header(msg.get("Subject", "")),
1055
- "received_at": (msg.get("Date") or "").strip(),
1056
- "in_reply_to": (msg.get("In-Reply-To") or "").strip(),
1057
- "thread_id": (msg.get("References") or msg.get("In-Reply-To") or "").strip().split()[-1] if msg.get("References") or msg.get("In-Reply-To") else "",
1067
+ "received_at": _decode_header(msg.get("Date", "")).strip(),
1068
+ "in_reply_to": in_reply_to_raw.strip(),
1069
+ "thread_id": thread_id,
1058
1070
  }
1059
1071
  except Exception as e:
1060
- log.debug(f"Header parse failed: {e}")
1072
+ log.warning(f"Header parse failed: {e}")
1061
1073
  return {}
1062
1074
 
1063
1075