nexo-brain 7.30.14 → 7.30.15
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.30.
|
|
3
|
+
"version": "7.30.15",
|
|
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.30.
|
|
21
|
+
Version `7.30.15` is the current packaged-runtime line. Patch release over v7.30.14 - Email NEXO reply lifecycle closure now handles affirmative actionable replies and prevents already-closed processed threads from reopening.
|
|
22
|
+
|
|
23
|
+
Previously in `7.30.14`: patch release over v7.30.13 - support tickets, provider capability discovery, task-close rearming, internal audit followups, and the memory-observation watchdog are aligned for Desktop-managed agents.
|
|
22
24
|
|
|
23
25
|
Previously in `7.30.13`: patch release over v7.30.12 - Email NEXO monitor prompts now require a full IMAP body fetch before replying, and managed email defaults use the MXroute `Sent` folder instead of `INBOX.Sent`.
|
|
24
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.15",
|
|
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",
|
|
@@ -109,6 +109,8 @@ CREATE INDEX IF NOT EXISTS idx_ee_email ON email_events(email_id);
|
|
|
109
109
|
CREATE INDEX IF NOT EXISTS idx_ee_event ON email_events(event);
|
|
110
110
|
CREATE INDEX IF NOT EXISTS idx_ee_ts ON email_events(timestamp);
|
|
111
111
|
"""
|
|
112
|
+
ACTION_CLOSURE_EVENTS = ("action_done", "resolution")
|
|
113
|
+
SENT_REPLY_EVENTS = ("action_done", "resolution", "replied")
|
|
112
114
|
EMAIL_LOOP_GUARD_SQL = """
|
|
113
115
|
CREATE TABLE IF NOT EXISTS email_loop_guards (
|
|
114
116
|
thread_key TEXT PRIMARY KEY,
|
|
@@ -902,20 +904,74 @@ def _has_active_processing_within(conn, email_id, *, hours=ZOMBIE_TIMEOUT_HOURS)
|
|
|
902
904
|
|
|
903
905
|
|
|
904
906
|
def _has_action_done_after(conn, email_id, reference_ts):
|
|
907
|
+
return _has_email_event_after(conn, email_id, ACTION_CLOSURE_EVENTS, reference_ts)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _has_email_event_after(conn, email_id, events, reference_ts):
|
|
911
|
+
if not email_id or not reference_ts:
|
|
912
|
+
return False
|
|
913
|
+
event_list = tuple(events or ())
|
|
914
|
+
if not event_list:
|
|
915
|
+
return False
|
|
916
|
+
placeholders = ",".join("?" for _ in event_list)
|
|
905
917
|
row = conn.execute(
|
|
906
|
-
"""
|
|
918
|
+
f"""
|
|
919
|
+
SELECT 1
|
|
920
|
+
FROM email_events
|
|
921
|
+
WHERE email_id = ?
|
|
922
|
+
AND event IN ({placeholders})
|
|
923
|
+
AND datetime(replace(timestamp, 'T', ' ')) > datetime(replace(?, 'T', ' '))
|
|
924
|
+
LIMIT 1
|
|
925
|
+
""",
|
|
926
|
+
(email_id, *event_list, reference_ts),
|
|
927
|
+
).fetchone()
|
|
928
|
+
return bool(row)
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def _has_sent_reply_event(conn, email_id):
|
|
932
|
+
if not email_id:
|
|
933
|
+
return False
|
|
934
|
+
placeholders = ",".join("?" for _ in SENT_REPLY_EVENTS)
|
|
935
|
+
row = conn.execute(
|
|
936
|
+
f"""
|
|
907
937
|
SELECT 1
|
|
908
938
|
FROM email_events
|
|
909
939
|
WHERE email_id = ?
|
|
910
|
-
AND event IN (
|
|
911
|
-
AND timestamp > ?
|
|
940
|
+
AND event IN ({placeholders})
|
|
912
941
|
LIMIT 1
|
|
913
942
|
""",
|
|
914
|
-
(email_id,
|
|
943
|
+
(email_id, *SENT_REPLY_EVENTS),
|
|
915
944
|
).fetchone()
|
|
916
945
|
return bool(row)
|
|
917
946
|
|
|
918
947
|
|
|
948
|
+
def _mark_processing_email_processed(conn, email_id, *, completed_at):
|
|
949
|
+
cols = _email_table_columns(conn)
|
|
950
|
+
updates = [
|
|
951
|
+
"status = 'processed'",
|
|
952
|
+
"started_at = NULL",
|
|
953
|
+
]
|
|
954
|
+
params = []
|
|
955
|
+
if "completed_at" in cols:
|
|
956
|
+
updates.append(
|
|
957
|
+
"completed_at = CASE WHEN completed_at IS NULL OR trim(completed_at) = '' THEN ? ELSE completed_at END"
|
|
958
|
+
)
|
|
959
|
+
params.append(completed_at)
|
|
960
|
+
if "error" in cols:
|
|
961
|
+
updates.append("error = NULL")
|
|
962
|
+
params.append(email_id)
|
|
963
|
+
cur = conn.execute(
|
|
964
|
+
f"""
|
|
965
|
+
UPDATE emails
|
|
966
|
+
SET {', '.join(updates)}
|
|
967
|
+
WHERE message_id = ?
|
|
968
|
+
AND status = 'processing'
|
|
969
|
+
""",
|
|
970
|
+
tuple(params),
|
|
971
|
+
)
|
|
972
|
+
return cur.rowcount > 0
|
|
973
|
+
|
|
974
|
+
|
|
919
975
|
def scan_debt(db_path=EMAIL_DB_PATH, *, max_items=5):
|
|
920
976
|
conn = sqlite3.connect(db_path)
|
|
921
977
|
conn.row_factory = sqlite3.Row
|
|
@@ -998,7 +1054,12 @@ def scan_debt(db_path=EMAIL_DB_PATH, *, max_items=5):
|
|
|
998
1054
|
"""
|
|
999
1055
|
).fetchall()
|
|
1000
1056
|
recovered = []
|
|
1057
|
+
sent_reconciled = []
|
|
1001
1058
|
for row in stuck_rows:
|
|
1059
|
+
if _has_sent_reply_event(conn, row["message_id"]):
|
|
1060
|
+
if _mark_processing_email_processed(conn, row["message_id"], completed_at=now_label):
|
|
1061
|
+
sent_reconciled.append(row)
|
|
1062
|
+
continue
|
|
1002
1063
|
conn.execute(
|
|
1003
1064
|
"""
|
|
1004
1065
|
UPDATE emails
|
|
@@ -1053,6 +1114,11 @@ def scan_debt(db_path=EMAIL_DB_PATH, *, max_items=5):
|
|
|
1053
1114
|
if recovered:
|
|
1054
1115
|
lines.append("")
|
|
1055
1116
|
lines.append(f"Auto-recovery applied: {len(recovered)} processing-stuck email(s) were reset to pending.")
|
|
1117
|
+
if sent_reconciled:
|
|
1118
|
+
lines.append("")
|
|
1119
|
+
lines.append(
|
|
1120
|
+
f"Reconciled {len(sent_reconciled)} processing email(s) with already-sent reply events; no re-open applied."
|
|
1121
|
+
)
|
|
1056
1122
|
total_reconciled = len(live_reconciled) + len(finished_reconciled)
|
|
1057
1123
|
if total_reconciled:
|
|
1058
1124
|
lines.append(f"Reconciled {total_reconciled} email(s) with inconsistent lifecycle state.")
|
|
@@ -1665,7 +1731,7 @@ def _recover_unreplied_processed(config, hours=24):
|
|
|
1665
1731
|
AND NOT EXISTS (
|
|
1666
1732
|
SELECT 1 FROM email_events ev
|
|
1667
1733
|
WHERE ev.email_id = e.message_id
|
|
1668
|
-
AND ev.event IN ('replied', 'resolution')
|
|
1734
|
+
AND ev.event IN ('replied', 'resolution', 'action_done')
|
|
1669
1735
|
)
|
|
1670
1736
|
ORDER BY e.rowid DESC
|
|
1671
1737
|
LIMIT 20
|
|
@@ -270,7 +270,7 @@ def _has_open_action(conn, email_id):
|
|
|
270
270
|
return not latest_done or latest_done < latest_open
|
|
271
271
|
|
|
272
272
|
|
|
273
|
-
def record_reply_lifecycle(in_reply_to, references, body_text, *, subject="", to="", cc="", message_id="", db_path=EMAIL_DB_PATH):
|
|
273
|
+
def record_reply_lifecycle(in_reply_to, references, body_text, *, subject="", to="", cc="", message_id="", db_path=EMAIL_DB_PATH, classify_override=""):
|
|
274
274
|
try:
|
|
275
275
|
conn = sqlite3.connect(db_path)
|
|
276
276
|
conn.row_factory = sqlite3.Row
|
|
@@ -280,7 +280,7 @@ def record_reply_lifecycle(in_reply_to, references, body_text, *, subject="", to
|
|
|
280
280
|
conn.close()
|
|
281
281
|
return None
|
|
282
282
|
|
|
283
|
-
event = classify_reply_event(body_text)
|
|
283
|
+
event = (classify_override or "").strip() or classify_reply_event(body_text)
|
|
284
284
|
detail = normalize_reply_text(body_text)[:200]
|
|
285
285
|
meta = json.dumps(
|
|
286
286
|
{
|
|
@@ -476,6 +476,18 @@ def build_parser():
|
|
|
476
476
|
parser.add_argument("--quote-date", default="")
|
|
477
477
|
parser.add_argument("--thread-file", default="", help="Full thread history file (all previous messages)")
|
|
478
478
|
parser.add_argument("--attach", "--attachment", dest="attach", action="append", default=[], help="File to attach (can repeat)")
|
|
479
|
+
parser.add_argument(
|
|
480
|
+
"--classify-as",
|
|
481
|
+
dest="classify_as",
|
|
482
|
+
default="",
|
|
483
|
+
choices=["", "resolution", "ack", "commitment", "replied", "debt_flagged"],
|
|
484
|
+
help=(
|
|
485
|
+
"Force the reply lifecycle event instead of heuristic auto-classification. "
|
|
486
|
+
"Use when the reply is substantive but opens with a short affirmation that the "
|
|
487
|
+
"text heuristic would mis-read as 'ack' (escalation -> debt_flagged). "
|
|
488
|
+
"Empty (default) keeps automatic classification."
|
|
489
|
+
),
|
|
490
|
+
)
|
|
479
491
|
return parser
|
|
480
492
|
|
|
481
493
|
|
|
@@ -536,6 +548,7 @@ def main(argv=None):
|
|
|
536
548
|
to=args.to,
|
|
537
549
|
cc=args.cc,
|
|
538
550
|
message_id=msg_id,
|
|
551
|
+
classify_override=(args.classify_as or "").strip(),
|
|
539
552
|
)
|
|
540
553
|
try:
|
|
541
554
|
record_sent_email(
|
|
@@ -40,6 +40,9 @@ You MUST register read-side lifecycle events, not send-side events:
|
|
|
40
40
|
- When you open/analyze a new email seriously, add an `opened` event for that `message_id`.
|
|
41
41
|
- When you change `emails.status` to `processing`, also add a `processing` event.
|
|
42
42
|
- Do NOT register `ack` / `commitment` / `resolution` manually when replying: `nexo-send-reply.py` already does that.
|
|
43
|
+
- When the reply closes or answers a real actionable instruction, pass `--classify-as resolution`.
|
|
44
|
+
This is mandatory when the body starts with a short affirmation (`sí`, `de acuerdo`, `perfecto`, etc.) but then contains substantive instructions or completed work.
|
|
45
|
+
- Use `--classify-as ack` only for a pure receipt/started-working acknowledgement, and `--classify-as commitment` only for a real future commitment.
|
|
43
46
|
- Use sqlite3 or local python3+sqlite3; tracking is best-effort, append-only, and never deletes historical entries.
|
|
44
47
|
|
|
45
48
|
== WHEN THERE IS DEBT BUT NO UNREAD EMAIL ==
|
|
@@ -169,7 +172,9 @@ Mandatory steps before sending:
|
|
|
169
172
|
The bottom of the email must preserve message -> reply -> message -> reply without dropping previous answers.
|
|
170
173
|
|
|
171
174
|
== SEND VIA `nexo-send-reply.py` ==
|
|
172
|
-
[[python_executable]] [[send_reply_script]] --to X --cc Y --subject 'Re: Z' --in-reply-to '<msgid>' --references '<refs>' --body-file /tmp/nexo-reply-UID.txt --quote-file /tmp/nexo-quote-UID.txt --quote-from 'Name <email>' --quote-date 'date' --thread-file /tmp/nexo-thread-UID.txt [--attach /path/to/file]
|
|
175
|
+
[[python_executable]] [[send_reply_script]] --to X --cc Y --subject 'Re: Z' --in-reply-to '<msgid>' --references '<refs>' --body-file /tmp/nexo-reply-UID.txt --quote-file /tmp/nexo-quote-UID.txt --quote-from 'Name <email>' --quote-date 'date' --thread-file /tmp/nexo-thread-UID.txt --classify-as resolution [--attach /path/to/file]
|
|
176
|
+
|
|
177
|
+
Use a different `--classify-as` value only when the lifecycle is truly different (`ack`, `commitment`, `replied`, `debt_flagged`).
|
|
173
178
|
|
|
174
179
|
== ANTI-LOOP PROTECTION ==
|
|
175
180
|
Do not reply to auto-replies, [[agent_email_label]] itself, `noreply@`,
|