nexo-brain 7.31.7 → 7.31.9
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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/plugins/protocol.py +74 -0
- package/src/scripts/nexo-email-monitor.py +103 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.31.
|
|
3
|
+
"version": "7.31.9",
|
|
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.31.
|
|
21
|
+
Version `7.31.9` is the current packaged-runtime line. Patch release over v7.31.8 - UI release closeout now has to prove the original reported symptom was reopened with observable evidence before claiming the release is ready.
|
|
22
|
+
|
|
23
|
+
Previously in `7.31.8`: patch release over v7.31.7 - email monitor debt scans no longer escalate intentionally waiting threads as unresolved commitments when recent resolution or hot-context state proves the thread is waiting on the user or a third party. Real unresolved commitments still surface.
|
|
24
|
+
|
|
25
|
+
Previously in `7.31.7`: patch release over v7.31.6 - stateful answers now require evidence before claiming release, commit, branch, server, ticket, deployment, sent/uploaded, installed, verified, or closed status. Guardian defaults promote identity coherence to hard/core and add a hard/core pre-answer evidence gate, while closeout and Local Context telemetry now leave stronger proof trails.
|
|
22
26
|
|
|
23
27
|
Previously in `7.31.6`: patch release over v7.31.5 - headless/email-monitor notifications now respect the Desktop UI language for static ES/EN templates before falling back to profile/calibration language and English.
|
|
24
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.31.
|
|
3
|
+
"version": "7.31.9",
|
|
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",
|
package/src/plugins/protocol.py
CHANGED
|
@@ -676,6 +676,42 @@ def _has_live_surface_verification(evidence: str) -> bool:
|
|
|
676
676
|
return bool(re.search(r"\b(producci[oó]n|production|live)\b.*\b(logs?|bd|db|browser|navegador|playwright|url|http|tema)\b", text))
|
|
677
677
|
|
|
678
678
|
|
|
679
|
+
UI_CHANGE_HINT_RE = re.compile(
|
|
680
|
+
r"\b(ui|ux|front[- ]?end|frontend|renderer|web|browser|navegador|pantalla|modal|selector|bot[oó]n|button|css|html|react|vue|electron|theme|tema|storefront)\b",
|
|
681
|
+
re.IGNORECASE,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
RELEASE_READY_CLAIM_RE = re.compile(
|
|
685
|
+
r"\b(release\s+lista|feature\s+completa|desplegado\s+ok|despliegue\s+ok|lista\s+para\s+release|release\s+ready|feature\s+complete|deployed\s+ok)\b",
|
|
686
|
+
re.IGNORECASE,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
ORIGINAL_SYMPTOM_REPRO_RE = re.compile(
|
|
690
|
+
r"\b(s[ií]ntoma|symptom|original|repro|reproduj|reproduc|reportad[oa]|observad[oa])\b",
|
|
691
|
+
re.IGNORECASE,
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
ORIGINAL_SYMPTOM_EVIDENCE_RE = re.compile(
|
|
695
|
+
r"\b(curl|http\s*200|url\s+(?:p[uú]blica|repro)|screenshot|captura|headed|browser|navegador|playwright)\b",
|
|
696
|
+
re.IGNORECASE,
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _requires_original_symptom_verification(task: dict, closure_text: str) -> bool:
|
|
701
|
+
if not RELEASE_READY_CLAIM_RE.search(closure_text or ""):
|
|
702
|
+
return False
|
|
703
|
+
combined = " ".join(
|
|
704
|
+
str(task.get(field) or "")
|
|
705
|
+
for field in ("goal", "area", "project_hint", "context_hint", "verification_step", "files")
|
|
706
|
+
)
|
|
707
|
+
return bool(UI_CHANGE_HINT_RE.search(combined))
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _has_original_symptom_verification(evidence: str) -> bool:
|
|
711
|
+
text = evidence or ""
|
|
712
|
+
return bool(ORIGINAL_SYMPTOM_REPRO_RE.search(text) and ORIGINAL_SYMPTOM_EVIDENCE_RE.search(text))
|
|
713
|
+
|
|
714
|
+
|
|
679
715
|
def _requires_production_change_log(
|
|
680
716
|
task: dict,
|
|
681
717
|
outcome: str,
|
|
@@ -1870,6 +1906,44 @@ def handle_task_close(
|
|
|
1870
1906
|
indent=2,
|
|
1871
1907
|
)
|
|
1872
1908
|
|
|
1909
|
+
original_symptom_evidence = "\n".join(
|
|
1910
|
+
part
|
|
1911
|
+
for part in (clean_evidence, clean_change_verify, outcome_notes, verification, summary, result)
|
|
1912
|
+
if part
|
|
1913
|
+
)
|
|
1914
|
+
if (
|
|
1915
|
+
clean_outcome == "done"
|
|
1916
|
+
and _requires_original_symptom_verification(task, closure_text)
|
|
1917
|
+
and not _has_original_symptom_verification(original_symptom_evidence)
|
|
1918
|
+
):
|
|
1919
|
+
debt = _ensure_open_debt(
|
|
1920
|
+
task["session_id"],
|
|
1921
|
+
task_id,
|
|
1922
|
+
"verify_original_symptom_missing",
|
|
1923
|
+
severity="error",
|
|
1924
|
+
evidence=(
|
|
1925
|
+
"UI release-ready claim attempted without evidence that the original symptom was reproduced. "
|
|
1926
|
+
f"Goal: {task.get('goal','')}. Evidence provided: {original_symptom_evidence[:240]!r}"
|
|
1927
|
+
),
|
|
1928
|
+
debts=debts_created,
|
|
1929
|
+
)
|
|
1930
|
+
return json.dumps(
|
|
1931
|
+
{
|
|
1932
|
+
"ok": False,
|
|
1933
|
+
"error": "Cannot close UI release as done without original-symptom verification evidence.",
|
|
1934
|
+
"hint": (
|
|
1935
|
+
"Attach proof that the reported symptom itself was reopened and reproduced/verified: "
|
|
1936
|
+
"repro URL, headed screenshot/browser evidence, Playwright output, or curl output."
|
|
1937
|
+
),
|
|
1938
|
+
"task_id": task_id,
|
|
1939
|
+
"blocked_by": "verify_original_symptom",
|
|
1940
|
+
"debt_id": debt.get("id"),
|
|
1941
|
+
"debt_type": "verify_original_symptom_missing",
|
|
1942
|
+
},
|
|
1943
|
+
ensure_ascii=False,
|
|
1944
|
+
indent=2,
|
|
1945
|
+
)
|
|
1946
|
+
|
|
1873
1947
|
live_surface_required = _requires_live_surface_verification(task, clean_outcome)
|
|
1874
1948
|
live_surface_evidence = "\n".join(
|
|
1875
1949
|
part
|
|
@@ -183,6 +183,13 @@ CREATE INDEX IF NOT EXISTS idx_ee_ts ON email_events(timestamp);
|
|
|
183
183
|
"""
|
|
184
184
|
ACTION_CLOSURE_EVENTS = ("action_done", "resolution")
|
|
185
185
|
SENT_REPLY_EVENTS = ("action_done", "resolution", "replied")
|
|
186
|
+
DEBT_RESOLUTION_SUPPRESSION_KEYWORDS = (
|
|
187
|
+
"expected_wait_third_party",
|
|
188
|
+
"waiting_third_party",
|
|
189
|
+
"waiting_user",
|
|
190
|
+
"wakeup queue",
|
|
191
|
+
)
|
|
192
|
+
DEBT_WAITING_HOT_CONTEXT_STATES = ("waiting_third_party", "waiting_user")
|
|
186
193
|
EMAIL_LOOP_GUARD_SQL = """
|
|
187
194
|
CREATE TABLE IF NOT EXISTS email_loop_guards (
|
|
188
195
|
thread_key TEXT PRIMARY KEY,
|
|
@@ -959,6 +966,98 @@ def _debt_suppressed_recently(conn, email_id):
|
|
|
959
966
|
return _recent_debt_flagged(conn, email_id, hours=DEBT_WAKE_COOLDOWN_HOURS)
|
|
960
967
|
|
|
961
968
|
|
|
969
|
+
def _debt_suppressed_by_recent_resolution(conn, email_id, *, hours=12):
|
|
970
|
+
if not email_id:
|
|
971
|
+
return False
|
|
972
|
+
haystack = " || ' ' || ".join(
|
|
973
|
+
[
|
|
974
|
+
"COALESCE(detail, '')",
|
|
975
|
+
"COALESCE(meta, '')",
|
|
976
|
+
]
|
|
977
|
+
)
|
|
978
|
+
keyword_clause = " OR ".join(f"LOWER({haystack}) LIKE ?" for _ in DEBT_RESOLUTION_SUPPRESSION_KEYWORDS)
|
|
979
|
+
row = conn.execute(
|
|
980
|
+
f"""
|
|
981
|
+
SELECT 1
|
|
982
|
+
FROM email_events
|
|
983
|
+
WHERE email_id = ?
|
|
984
|
+
AND event = 'resolution'
|
|
985
|
+
AND timestamp >= datetime('now','localtime', ?)
|
|
986
|
+
AND ({keyword_clause})
|
|
987
|
+
LIMIT 1
|
|
988
|
+
""",
|
|
989
|
+
(
|
|
990
|
+
email_id,
|
|
991
|
+
f"-{hours} hours",
|
|
992
|
+
*[f"%{keyword.lower()}%" for keyword in DEBT_RESOLUTION_SUPPRESSION_KEYWORDS],
|
|
993
|
+
),
|
|
994
|
+
).fetchone()
|
|
995
|
+
return bool(row)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _default_nexo_db_path():
|
|
999
|
+
override = os.environ.get("NEXO_DB") or os.environ.get("NEXO_TEST_DB")
|
|
1000
|
+
if override:
|
|
1001
|
+
return Path(override)
|
|
1002
|
+
return NEXO_HOME / "runtime" / "data" / "nexo.db"
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def _email_context_keys(email_id):
|
|
1006
|
+
raw = str(email_id or "").strip()
|
|
1007
|
+
stripped = raw.strip("<>")
|
|
1008
|
+
keys = {raw}
|
|
1009
|
+
if stripped:
|
|
1010
|
+
keys.add(stripped)
|
|
1011
|
+
keys.add(f"<{stripped}>")
|
|
1012
|
+
keys.add(f"email:{stripped}")
|
|
1013
|
+
keys.add(f"email:<{stripped}>")
|
|
1014
|
+
return tuple(key for key in keys if key)
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def _debt_suppressed_by_waiting_hot_context(email_id, *, db_path=None, now_epoch=None):
|
|
1018
|
+
if not email_id:
|
|
1019
|
+
return False
|
|
1020
|
+
path = Path(db_path) if db_path else _default_nexo_db_path()
|
|
1021
|
+
if not path.exists():
|
|
1022
|
+
return False
|
|
1023
|
+
keys = _email_context_keys(email_id)
|
|
1024
|
+
if not keys:
|
|
1025
|
+
return False
|
|
1026
|
+
key_placeholders = ",".join("?" for _ in keys)
|
|
1027
|
+
state_placeholders = ",".join("?" for _ in DEBT_WAITING_HOT_CONTEXT_STATES)
|
|
1028
|
+
now_value = float(now_epoch if now_epoch is not None else time.time())
|
|
1029
|
+
try:
|
|
1030
|
+
conn = sqlite3.connect(str(path))
|
|
1031
|
+
try:
|
|
1032
|
+
row = conn.execute(
|
|
1033
|
+
f"""
|
|
1034
|
+
SELECT 1
|
|
1035
|
+
FROM hot_context
|
|
1036
|
+
WHERE state IN ({state_placeholders})
|
|
1037
|
+
AND expires_at > ?
|
|
1038
|
+
AND (
|
|
1039
|
+
(source_type = 'email' AND source_id IN ({key_placeholders}))
|
|
1040
|
+
OR context_key IN ({key_placeholders})
|
|
1041
|
+
)
|
|
1042
|
+
LIMIT 1
|
|
1043
|
+
""",
|
|
1044
|
+
(*DEBT_WAITING_HOT_CONTEXT_STATES, now_value, *keys, *keys),
|
|
1045
|
+
).fetchone()
|
|
1046
|
+
return bool(row)
|
|
1047
|
+
finally:
|
|
1048
|
+
conn.close()
|
|
1049
|
+
except sqlite3.Error as exc:
|
|
1050
|
+
log.debug("Debt hot_context suppression skipped: %s", exc)
|
|
1051
|
+
return False
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def _debt_suppressed_by_waiting_state(conn, email_id):
|
|
1055
|
+
return (
|
|
1056
|
+
_debt_suppressed_by_recent_resolution(conn, email_id)
|
|
1057
|
+
or _debt_suppressed_by_waiting_hot_context(email_id)
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
|
|
962
1061
|
def _has_active_processing_within(conn, email_id, *, hours=ZOMBIE_TIMEOUT_HOURS):
|
|
963
1062
|
row = conn.execute(
|
|
964
1063
|
"""
|
|
@@ -1074,6 +1173,8 @@ def scan_debt(db_path=EMAIL_DB_PATH, *, max_items=5):
|
|
|
1074
1173
|
continue
|
|
1075
1174
|
if _has_action_done_after(conn, row["email_id"], row["last_ack_ts"]):
|
|
1076
1175
|
continue
|
|
1176
|
+
if _debt_suppressed_by_waiting_state(conn, row["email_id"]):
|
|
1177
|
+
continue
|
|
1077
1178
|
if _debt_suppressed_recently(conn, row["email_id"]):
|
|
1078
1179
|
continue
|
|
1079
1180
|
items.append(
|
|
@@ -1101,6 +1202,8 @@ def scan_debt(db_path=EMAIL_DB_PATH, *, max_items=5):
|
|
|
1101
1202
|
continue
|
|
1102
1203
|
if _has_action_done_after(conn, row["email_id"], row["last_commitment_ts"]):
|
|
1103
1204
|
continue
|
|
1205
|
+
if _debt_suppressed_by_waiting_state(conn, row["email_id"]):
|
|
1206
|
+
continue
|
|
1104
1207
|
if _debt_suppressed_recently(conn, row["email_id"]):
|
|
1105
1208
|
continue
|
|
1106
1209
|
items.append(
|