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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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"
|
|
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"
|
|
1056
|
-
"in_reply_to":
|
|
1057
|
-
"thread_id":
|
|
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.
|
|
1072
|
+
log.warning(f"Header parse failed: {e}")
|
|
1061
1073
|
return {}
|
|
1062
1074
|
|
|
1063
1075
|
|