nexo-brain 7.31.13 → 7.32.0

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.31.13",
3
+ "version": "7.32.0",
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 `7.31.13` is the current packaged-runtime line. Patch release over v7.31.12 - the offline wheel bundle no longer hard-pins onnxruntime, so cross-platform/offline installs (including older Linux) resolve a compatible native wheel instead of failing. Version `7.31.11` was a patch release over v7.31.10 - MCP lifecycle robustness + guardrail precision.
21
+ Version `7.32.0` is the current packaged-runtime line. Minor release - Cognitive OS Ola 1: the causal/provenance graph now populates from every evidence-backed task close (the connect-the-dots substrate that was previously empty), and abandoned durable workflows are reaped so the cross-session resume surface stays clean. Bundles the 7.31.14 critical fixes (cron-fleet drift guard, deterministic spreading-activation id, real guard_context evidence, auto error->learning capture, fulfillable confidence-check gate, approval-paused workflow resume).
22
22
 
23
23
  Previously in `7.31.9`: 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.
24
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.31.13",
3
+ "version": "7.32.0",
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",
@@ -182,6 +182,43 @@ def auto_close_open_protocol_tasks(conn, sid: str, task: str = "") -> list[str]:
182
182
  return closed
183
183
 
184
184
 
185
+ def auto_close_abandoned_workflow_runs(conn, sid: str) -> dict:
186
+ """Reap durable workflow_runs / workflow_goals abandoned by a stale session.
187
+
188
+ auto_close only reaped protocol_tasks; a session that opened a durable
189
+ workflow_run / workflow_goal and never closed it left a zombie 'running'
190
+ row forever, polluting the resume surface (M10 gap). Move non-terminal ones
191
+ to a terminal state when their owning session is reaped. closed_at/updated_at
192
+ use datetime('now') to match the workflow tables' timestamp format.
193
+ """
194
+ note = "auto-close: stale session ended without explicit workflow close"
195
+ runs = conn.execute(
196
+ "SELECT run_id FROM workflow_runs "
197
+ "WHERE session_id = ? AND status IN ('open','running','blocked','waiting_approval')",
198
+ (sid,),
199
+ ).fetchall()
200
+ for row in runs:
201
+ conn.execute(
202
+ "UPDATE workflow_runs SET status='cancelled', next_action=?, "
203
+ "closed_at=datetime('now'), updated_at=datetime('now') "
204
+ "WHERE run_id=? AND status IN ('open','running','blocked','waiting_approval')",
205
+ (note, row["run_id"]),
206
+ )
207
+ goals = conn.execute(
208
+ "SELECT goal_id FROM workflow_goals "
209
+ "WHERE session_id = ? AND status IN ('active','blocked')",
210
+ (sid,),
211
+ ).fetchall()
212
+ for row in goals:
213
+ conn.execute(
214
+ "UPDATE workflow_goals SET status='abandoned', blocker_reason=?, "
215
+ "closed_at=datetime('now'), updated_at=datetime('now') "
216
+ "WHERE goal_id=? AND status IN ('active','blocked')",
217
+ (note, row["goal_id"]),
218
+ )
219
+ return {"runs": len(runs), "goals": len(goals)}
220
+
221
+
185
222
  def main():
186
223
  init_db()
187
224
  conn = get_db()
@@ -197,6 +234,7 @@ def main():
197
234
  draft = get_diary_draft(sid)
198
235
  closed_tasks = auto_close_open_protocol_tasks(conn, sid, task=session.get("task", ""))
199
236
  closed_task_ids.extend(closed_tasks)
237
+ auto_close_abandoned_workflow_runs(conn, sid)
200
238
 
201
239
  if draft:
202
240
  promote_draft_to_diary(sid, draft, task=session.get("task", ""))
@@ -784,8 +784,19 @@ CO_ACTIVATION_MIN_STRENGTH = 0.1
784
784
 
785
785
 
786
786
  def _canonical_co_id(store: str, mid: int) -> int:
787
- """Create a canonical hash ID for co-activation tracking."""
788
- return hash(f"{store}:{mid}") % (2**31)
787
+ """Create a canonical, PROCESS-STABLE hash ID for co-activation tracking.
788
+
789
+ MUST be deterministic across processes. Python's builtin hash() is salted
790
+ per process (PYTHONHASHSEED), so co-activation links written in one MCP
791
+ process never matched the same memory's id in the next — fragmenting the
792
+ associative graph (observed ~6x distinct ids per memory) and silently
793
+ degrading spreading activation to within-a-single-process-lifetime. blake2b
794
+ is stable across processes and runs.
795
+ """
796
+ import hashlib
797
+
798
+ digest = hashlib.blake2b(f"{store}:{mid}".encode("utf-8"), digest_size=8).digest()
799
+ return int.from_bytes(digest, "big") % (2**31)
789
800
 
790
801
 
791
802
  def record_co_activation(memory_ids: list[tuple[str, int]]):
package/src/crons/sync.py CHANGED
@@ -727,13 +727,18 @@ def install_plist(label: str, plist: dict, plist_path: Path, dry_run: bool):
727
727
  log(f" DRY-RUN: would install {plist_path.name}")
728
728
  return
729
729
 
730
- with open(plist_path, "wb") as f:
731
- plistlib.dump(plist, f)
732
-
730
+ # Ephemeral/test runtimes (temp NEXO_HOME or HOME, e.g. a pytest run) must
731
+ # NOT touch the operator's real ~/Library/LaunchAgents. The guard is checked
732
+ # BEFORE writing the plist file: otherwise a test run rewrites the real
733
+ # plists with temp-dir ProgramArguments, and one reboot/reload silently
734
+ # kills the whole consolidation cron fleet (cron-fleet-drift incident).
733
735
  if not launchctl_side_effects_allowed():
734
- log(f" Installed but skipped launchctl in ephemeral runtime: {plist_path.name}")
736
+ log(f" Skipped plist write in ephemeral runtime: {plist_path.name}")
735
737
  return
736
738
 
739
+ with open(plist_path, "wb") as f:
740
+ plistlib.dump(plist, f)
741
+
737
742
  result = reload_launchagent_plist(plist_path, label=label)
738
743
  if result.get("action") == "skipped-ephemeral-runtime":
739
744
  log(f" Installed but skipped launchctl in ephemeral runtime: {plist_path.name}")
@@ -751,8 +756,8 @@ def unload_plist(plist_path: Path, dry_run: bool):
751
756
  return
752
757
 
753
758
  if not launchctl_side_effects_allowed():
754
- plist_path.unlink(missing_ok=True)
755
- log(f" Removed without launchctl in ephemeral runtime: {plist_path.name}")
759
+ # Ephemeral/test runtime: never delete the operator's real plists.
760
+ log(f" Skipped plist removal in ephemeral runtime: {plist_path.name}")
756
761
  return
757
762
 
758
763
  result = unload_launchagent_plist(plist_path)
@@ -830,7 +835,9 @@ def sync(dry_run: bool = False):
830
835
  return
831
836
 
832
837
  LOG_DIR.mkdir(parents=True, exist_ok=True)
833
- LAUNCH_AGENTS_DIR.mkdir(parents=True, exist_ok=True)
838
+ # In an ephemeral/test runtime, do not even create the real LaunchAgents dir.
839
+ if launchctl_side_effects_allowed():
840
+ LAUNCH_AGENTS_DIR.mkdir(parents=True, exist_ok=True)
834
841
 
835
842
  manifest_crons = load_manifest()
836
843
  manifest_ids = {c["id"] for c in manifest_crons}
package/src/db/_schema.py CHANGED
@@ -3081,6 +3081,37 @@ def _m81_core_rules_product_metadata(conn):
3081
3081
  conn.execute("CREATE INDEX IF NOT EXISTS idx_core_rules_protected ON core_rules(protected, is_active)")
3082
3082
 
3083
3083
 
3084
+ def _m82_confidence_checks(conn):
3085
+ """Persist nexo_confidence_check calls so the answer-contract gate works.
3086
+
3087
+ The G1 enforcer (hooks/g1_enforcer.py) treats a verify/ask/defer contract as
3088
+ fulfilled when a confidence_checks row exists for the session created after
3089
+ the task opened. No code ever created or wrote this table, so verify
3090
+ contracts were structurally unfulfillable. handle_confidence_check now writes
3091
+ a row per call; created_at uses datetime('now') to match opened_at format.
3092
+ """
3093
+ conn.execute(
3094
+ """
3095
+ CREATE TABLE IF NOT EXISTS confidence_checks (
3096
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3097
+ session_id TEXT,
3098
+ task_id TEXT,
3099
+ goal_hash TEXT,
3100
+ task_type TEXT,
3101
+ area TEXT,
3102
+ response_mode TEXT,
3103
+ confidence INTEGER,
3104
+ high_stakes INTEGER NOT NULL DEFAULT 0,
3105
+ created_at TEXT
3106
+ )
3107
+ """
3108
+ )
3109
+ conn.execute(
3110
+ "CREATE INDEX IF NOT EXISTS idx_confidence_checks_session "
3111
+ "ON confidence_checks(session_id, created_at)"
3112
+ )
3113
+
3114
+
3084
3115
  MIGRATIONS = [
3085
3116
  (1, "learnings_columns", _m1_learnings_columns),
3086
3117
  (2, "followups_reasoning", _m2_followups_reasoning),
@@ -3163,6 +3194,7 @@ MIGRATIONS = [
3163
3194
  (79, "operational_closure_links_readiness", _m79_operational_closure_links_readiness),
3164
3195
  (80, "opportunity_orchestrator", _m80_opportunity_orchestrator),
3165
3196
  (81, "core_rules_product_metadata", _m81_core_rules_product_metadata),
3197
+ (82, "confidence_checks", _m82_confidence_checks),
3166
3198
  ]
3167
3199
 
3168
3200
 
@@ -289,7 +289,7 @@ def _content_hash(fact_type: str, content: str) -> str:
289
289
 
290
290
 
291
291
  def _auto_learning_add(title: str, content: str) -> bool:
292
- """Best-effort call to tools_learnings.add_learning.
292
+ """Best-effort call to tools_learnings.handle_learning_add.
293
293
 
294
294
  Returns True when the learning was stored, False otherwise. Failures
295
295
  are silent so the hook itself never breaks the user's prompt flow.
@@ -300,13 +300,19 @@ def _auto_learning_add(title: str, content: str) -> bool:
300
300
  return False
301
301
 
302
302
  try:
303
- result = tools_learnings.add_learning(
303
+ # The public symbol is handle_learning_add. A prior call to a
304
+ # non-existent tools_learnings.add_learning raised AttributeError that
305
+ # was swallowed below, so EVERY auto-captured correction silently
306
+ # failed to persist a learning (error-capture / never-repeat broken).
307
+ result = tools_learnings.handle_learning_add(
304
308
  category="auto",
305
309
  title=title,
306
310
  content=content,
307
311
  priority="medium",
308
312
  reasoning="auto-captured from correction pattern in UserPromptSubmit/PostToolUse hook",
309
313
  )
314
+ if isinstance(result, str):
315
+ return not result.strip().upper().startswith("ERROR")
310
316
  if isinstance(result, dict):
311
317
  return bool(result.get("ok") or result.get("id") or result.get("learning_id"))
312
318
  return bool(result)
@@ -1437,6 +1437,7 @@ def handle_confidence_check(
1437
1437
  unknowns: str = "[]",
1438
1438
  verification_step: str = "",
1439
1439
  stakes: str = "",
1440
+ sid: str = "",
1440
1441
  ) -> str:
1441
1442
  """Return the metacognitive response mode: answer, verify, ask, or defer."""
1442
1443
  clean_goal = (goal or "").strip()
@@ -1465,6 +1466,37 @@ def handle_confidence_check(
1465
1466
  verification_step=(verification_step or "").strip(),
1466
1467
  stakes=(stakes or "").strip(),
1467
1468
  )
1469
+ # Persist the check so the G1 answer-contract gate can detect fulfillment of
1470
+ # verify/ask/defer contracts (this table was previously never written, so
1471
+ # verify contracts were structurally unfulfillable). Best-effort: a failure
1472
+ # here must never break the metacognitive answer — g1 simply re-nudges, a
1473
+ # visible signal rather than a silent corruption.
1474
+ try:
1475
+ import hashlib
1476
+ from db import get_db
1477
+ from plugins.guard import _resolve_active_sid
1478
+ conn = get_db()
1479
+ resolved_sid = (sid or "").strip() or _resolve_active_sid(conn)
1480
+ if resolved_sid:
1481
+ conn.execute(
1482
+ """INSERT INTO confidence_checks
1483
+ (session_id, task_id, goal_hash, task_type, area,
1484
+ response_mode, confidence, high_stakes, created_at)
1485
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))""",
1486
+ (
1487
+ resolved_sid,
1488
+ "",
1489
+ hashlib.sha256(clean_goal.encode("utf-8")).hexdigest()[:16],
1490
+ clean_type,
1491
+ (area or "").strip(),
1492
+ str(result.get("mode") or ""),
1493
+ int(result.get("confidence") or 0),
1494
+ 1 if result.get("high_stakes") else 0,
1495
+ ),
1496
+ )
1497
+ conn.commit()
1498
+ except Exception:
1499
+ pass
1468
1500
  return json.dumps({"ok": True, **result}, ensure_ascii=False, indent=2)
1469
1501
 
1470
1502
 
@@ -2350,6 +2382,22 @@ def handle_task_close(
2350
2382
  debt_types=["missing_change_log"],
2351
2383
  resolution="Change log created by nexo_task_close",
2352
2384
  )
2385
+ # Cognitive OS Ola 1 — materialize causal/provenance edges from the
2386
+ # closed task (task→change_log "ops:produced" + change_log→task
2387
+ # "causal:motivated_by"). record_task_close_edges had NO caller, so
2388
+ # the causal graph stayed empty (0 candidates) and could never feed
2389
+ # connect-the-dots at answer time. Best-effort: graph wiring must
2390
+ # never break a task close.
2391
+ try:
2392
+ import causal_graph
2393
+ causal_graph.record_task_close_edges(
2394
+ task_id=task_id,
2395
+ change_log_id=change_log_id,
2396
+ project_key=str(task.get("project_hint") or task.get("area") or ""),
2397
+ reason_public=(clean_change_summary or task.get("goal") or "")[:200],
2398
+ )
2399
+ except Exception:
2400
+ pass
2353
2401
  else:
2354
2402
  debt = _ensure_open_debt(
2355
2403
  task["session_id"],
@@ -1987,15 +1987,48 @@ def _source_filesystem(request: SourceRequest) -> SourceResult:
1987
1987
 
1988
1988
 
1989
1989
  def _source_guard_context(request: SourceRequest) -> SourceResult:
1990
- # G01 cannot call the MCP guard from this pure core. Return the file scope
1991
- # so G15 can wire real guard context without changing the source plan.
1992
- if not request.files:
1990
+ # Real guard verification: surface the file-conditioned blocking learnings
1991
+ # for the requested files. Previously this returned fake evidence
1992
+ # (evidence_refs=["guard_context:requested"], result_count=1) WITHOUT any
1993
+ # check, which silently satisfied the critical-tier required-source / gap
1994
+ # gate for release/server/billing/legal areas. Never fake evidence again.
1995
+ files = [f.strip() for f in (request.files or "").split(",") if f.strip()]
1996
+ if not files:
1993
1997
  return SourceResult(source="guard_context")
1998
+ try:
1999
+ from db import get_db
2000
+ from plugins.guard import _load_conditioned_learnings
2001
+ conn = get_db()
2002
+ conditioned = _load_conditioned_learnings(conn, files)
2003
+ except Exception:
2004
+ # Fail-closed: do NOT fake evidence; report that verification could not run.
2005
+ return SourceResult(
2006
+ source="guard_context",
2007
+ rendered="Guard verification could not run for: " + ", ".join(files),
2008
+ result_count=0,
2009
+ )
2010
+ refs: list[str] = []
2011
+ lines: list[str] = []
2012
+ for filepath, entries in conditioned.items():
2013
+ for entry in entries:
2014
+ refs.append(f"learning:{entry.get('id')}")
2015
+ lines.append(
2016
+ f"- [{entry.get('priority', 'medium')}] {entry.get('title', '')} (applies_to {filepath})"
2017
+ )
2018
+ if lines:
2019
+ return SourceResult(
2020
+ source="guard_context",
2021
+ rendered="Blocking/file-conditioned learnings:\n" + "\n".join(lines),
2022
+ evidence_refs=refs,
2023
+ result_count=len(refs),
2024
+ )
2025
+ # Guard ran and found nothing blocking — a real verified-clean result.
1994
2026
  return SourceResult(
1995
2027
  source="guard_context",
1996
- rendered=f"Guard context requested for files: {request.files}",
1997
- evidence_refs=["guard_context:requested"],
1998
- result_count=1,
2028
+ rendered="Guard verified: no blocking file-conditioned learnings for "
2029
+ + ", ".join(files),
2030
+ evidence_refs=["guard_context:verified_clean"],
2031
+ result_count=0,
1999
2032
  )
2000
2033
 
2001
2034
 
@@ -317,7 +317,7 @@ def _session_portability_bundle(sid: str = "") -> dict:
317
317
  dict(row) for row in conn.execute(
318
318
  """SELECT run_id, goal_id, goal, workflow_kind, status, priority, next_action, current_step_key, updated_at
319
319
  FROM workflow_runs
320
- WHERE session_id = ? AND status IN ('open', 'running', 'blocked', 'needs_approval')
320
+ WHERE session_id = ? AND status IN ('open', 'running', 'blocked', 'waiting_approval')
321
321
  ORDER BY updated_at DESC
322
322
  LIMIT 10""",
323
323
  (session_id,),