nexo-brain 7.34.0 → 7.35.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.
@@ -2194,6 +2194,28 @@ def _run_protocol_debt_drain_inline() -> dict:
2194
2194
  }
2195
2195
 
2196
2196
 
2197
+ def _run_schema_abstraction_distill_inline() -> dict:
2198
+ """Ola 4 — distill recurring incident archetypes into diagnostic templates.
2199
+
2200
+ Runs the clustering pass over the failure-prevention ledger (+ self-error
2201
+ learnings) and mints/refreshes diagnostic templates idempotently. Guidance
2202
+ only; never blocks. Best-effort: any failure is reported, not raised.
2203
+ """
2204
+ try:
2205
+ import schema_abstraction as sa
2206
+
2207
+ report = sa.distill_templates()
2208
+ except Exception as exc:
2209
+ return {"ok": False, "error": f"distill_failed: {exc}"}
2210
+ return {
2211
+ "ok": bool(report.get("ok")),
2212
+ "templates_created": int(report.get("templates_created") or 0),
2213
+ "templates_refreshed": int(report.get("templates_refreshed") or 0),
2214
+ "incidents": int(report.get("incidents") or 0),
2215
+ "clusters": int(report.get("clusters") or 0),
2216
+ }
2217
+
2218
+
2197
2219
  def _sanitize_watchdog_registry_inline() -> dict:
2198
2220
  hash_registry = _hash_registry_path()
2199
2221
  if not hash_registry.exists():
@@ -2329,6 +2351,24 @@ def run_mechanical_autofixes():
2329
2351
  elif debt_drain.get("error"):
2330
2352
  finding("WARN", "autofix", f"Protocol debt drain inline failed: {debt_drain['error']}")
2331
2353
 
2354
+ # Ola 4 SCHEMA-ABSTRACTION: distill recurring-incident archetypes into
2355
+ # reusable diagnostic templates (idempotent). Surfaces only when a new
2356
+ # template is minted/refreshed; silence otherwise (anti-noise).
2357
+ distill = _run_schema_abstraction_distill_inline()
2358
+ if distill.get("ok"):
2359
+ created = int(distill.get("templates_created") or 0)
2360
+ refreshed = int(distill.get("templates_refreshed") or 0)
2361
+ if created or refreshed:
2362
+ finding(
2363
+ "INFO",
2364
+ "autofix",
2365
+ f"Schema-abstraction distilled diagnostic templates: "
2366
+ f"created={created}, refreshed={refreshed} "
2367
+ f"(from {int(distill.get('incidents') or 0)} incidents)",
2368
+ )
2369
+ elif distill.get("error"):
2370
+ finding("WARN", "autofix", f"Schema-abstraction distill inline failed: {distill['error']}")
2371
+
2332
2372
  if NEXO_DB.exists():
2333
2373
  conn = sqlite3.connect(str(NEXO_DB))
2334
2374
  conn.row_factory = sqlite3.Row
package/src/server.py CHANGED
@@ -1843,6 +1843,60 @@ def nexo_memory_maintenance(
1843
1843
  return handle_memory_maintenance(process_limit, retry_failed, backfill_sources, backfill_limit)
1844
1844
 
1845
1845
 
1846
+ @mcp.tool
1847
+ def nexo_memory_forget(
1848
+ value: str,
1849
+ mode: str = "secret",
1850
+ dry_run: bool = True,
1851
+ confirm: bool = False,
1852
+ use_regex: bool = False,
1853
+ reason: str = "",
1854
+ ) -> str:
1855
+ """SELECTIVE-FORGET: verifiably erase a revoked secret, or correct a fact.
1856
+
1857
+ Two modes, never mixed:
1858
+ * mode='secret' (HARD-FORGET): scrubs the secret from EVERY table of ALL
1859
+ LIVE DBs the agent retrieves from — nexo, cognitive, local-context,
1860
+ local-context-usage, and email — discovered by live introspection
1861
+ (sqlite_master + PRAGMA) over each subsystem's own canonical path
1862
+ resolver, not a curated list — plus every FTS index (incl.
1863
+ local_chunks_fts), on-disk transcripts, and legacy shadow DBs (cognitive
1864
+ + local-context). The row is DELETED where it IS the secret (or carries
1865
+ an embedding/FTS copy), and REDACTED in place where the secret is
1866
+ embedded in an otherwise-useful record (diary, item_history.note,
1867
+ local_chunks.text, entity_facts.value, change_log...). It then
1868
+ RE-ENUMERATES and RE-SCANS everything and only returns complete=True at
1869
+ total zero; any survivor (even in a table not anticipated) is reported
1870
+ as complete=False with its <db>.<table> location. Use ONLY for revoked
1871
+ credentials/secrets or toxic data. Destructive run requires
1872
+ dry_run=False AND confirm=True (a dry-run count is returned otherwise).
1873
+ HNSW persisted indices are invalidated. SCOPE: point-in-time backups /
1874
+ snapshots are NOT swept (retention) — rotate the secret; the report's
1875
+ backup_scope field states this so complete=True never overclaims.
1876
+ * mode='fact' (CORRECT-FACT): does NOT delete. Useful memory is preserved
1877
+ via the existing reversible supersede; correct it with
1878
+ nexo_learning_update / supersede instead.
1879
+
1880
+ Args:
1881
+ value: the literal secret value to forget.
1882
+ mode: 'secret' (default, hard) or 'fact' (soft, reversible).
1883
+ dry_run: when True (default) only count matches; never mutate.
1884
+ confirm: required (with dry_run=False) to actually delete in secret mode.
1885
+ use_regex: also match generic secret-shaped tokens (secret mode only).
1886
+ reason: free-text audit note recorded in the ledger.
1887
+ """
1888
+ import memory_forget
1889
+
1890
+ return memory_forget.handle_memory_forget(
1891
+ value=value,
1892
+ mode=mode,
1893
+ dry_run=dry_run,
1894
+ confirm=confirm,
1895
+ use_regex=use_regex,
1896
+ reason=reason,
1897
+ )
1898
+
1899
+
1846
1900
  @mcp.tool
1847
1901
  def nexo_memory_search(
1848
1902
  query: str,
@@ -320,13 +320,71 @@ def handle_credential_delete(service: str, key: str = '') -> str:
320
320
  return f"Credential deleted."
321
321
  return f"All BYOK credentials deleted ({removed} files)."
322
322
 
323
+ # Capture the secret value(s) BEFORE deletion so the forget sweep can scrub
324
+ # any grep-able copies that leaked into memory (FTS, KG, transcripts, etc.).
325
+ leaked_values: list[str] = []
326
+ try:
327
+ existing = get_credential(service, key if key else None) or []
328
+ for entry in existing:
329
+ val = str((entry or {}).get("value") or "").strip()
330
+ if len(val) >= 8:
331
+ leaked_values.append(val)
332
+ except Exception:
333
+ leaked_values = []
334
+
323
335
  deleted = delete_credential(service, key if key else None)
324
336
  if not deleted:
325
337
  target = f"{service}/{key}" if key else service
326
338
  return f"ERROR: No credentials found for '{target}'."
339
+
340
+ # Auto-trigger SELECTIVE-FORGET (hard, secret mode) over the revoked value(s)
341
+ # so revoking the credential leaves no leaked copies in memory. Best-effort:
342
+ # never let a sweep failure block the credential deletion.
343
+ forgotten = 0
344
+ swept = False
345
+ incomplete = False
346
+ residual_detail = ""
347
+ for val in leaked_values:
348
+ try:
349
+ import memory_forget
350
+
351
+ sweep = memory_forget.sweep_revoked_secret(val, reason=f"credential_deleted:{service}")
352
+ if sweep.get("destructive"):
353
+ swept = True
354
+ # Count BOTH deleted rows and redacted-in-place rows: a leaked
355
+ # copy embedded in a useful record is scrubbed via redaction, not
356
+ # deletion, so counting only deletions would under-report.
357
+ forgotten += int(sweep.get("deleted_total") or 0)
358
+ forgotten += int(sweep.get("redacted_total") or 0)
359
+ if not sweep.get("complete", True):
360
+ incomplete = True
361
+ verification = sweep.get("verification") or {}
362
+ survivors = {
363
+ **(verification.get("residual_stores") or {}),
364
+ **(verification.get("residual_fts") or {}),
365
+ }
366
+ if verification.get("residual_shadows"):
367
+ survivors["shadow_dbs"] = len(verification["residual_shadows"])
368
+ if verification.get("residual_transcripts"):
369
+ survivors["transcripts"] = len(verification["residual_transcripts"])
370
+ if survivors and not residual_detail:
371
+ residual_detail = ", ".join(f"{k}={v}" for k, v in survivors.items())
372
+ except Exception:
373
+ continue
374
+
375
+ suffix = ""
376
+ if swept:
377
+ suffix = f" Forget sweep removed {forgotten} leaked copy/copies from memory."
378
+ if incomplete:
379
+ suffix += (
380
+ " WARNING: forget sweep reported RESIDUAL matches — the secret is "
381
+ "STILL grep-able and was NOT fully removed. Verify manually"
382
+ )
383
+ suffix += f" ({residual_detail})." if residual_detail else "."
384
+
327
385
  if key:
328
- return f"Credential deleted."
329
- return f"All credentials for service deleted."
386
+ return f"Credential deleted.{suffix}"
387
+ return f"All credentials for service deleted.{suffix}"
330
388
 
331
389
 
332
390
  def handle_credential_list(service: str = '') -> str:
@@ -2052,6 +2052,72 @@
2052
2052
  },
2053
2053
  "triggers_after": []
2054
2054
  },
2055
+ "nexo_schema_abstraction_distill": {
2056
+ "description": "Distill recurring incident archetypes into reusable diagnostic templates (idempotent clustering pass)",
2057
+ "category": "failure_prevention",
2058
+ "source": "plugin:schema_abstraction",
2059
+ "requires": [
2060
+ "failure_prevention_cases"
2061
+ ],
2062
+ "provides": [
2063
+ "diagnostic_templates"
2064
+ ],
2065
+ "internal_calls": [
2066
+ "learning_resolver.candidate_similarity"
2067
+ ],
2068
+ "enforcement": {
2069
+ "level": "none",
2070
+ "rules": []
2071
+ },
2072
+ "triggers_after": []
2073
+ },
2074
+ "nexo_schema_abstraction_templates": {
2075
+ "description": "List distilled diagnostic templates",
2076
+ "category": "failure_prevention",
2077
+ "source": "plugin:schema_abstraction",
2078
+ "requires": [
2079
+ "diagnostic_templates"
2080
+ ],
2081
+ "provides": [],
2082
+ "internal_calls": [],
2083
+ "enforcement": {
2084
+ "level": "none",
2085
+ "rules": []
2086
+ },
2087
+ "triggers_after": []
2088
+ },
2089
+ "nexo_schema_abstraction_match": {
2090
+ "description": "Match an action against diagnostic-template archetypes (primed diagnosis)",
2091
+ "category": "failure_prevention",
2092
+ "source": "plugin:schema_abstraction",
2093
+ "requires": [
2094
+ "diagnostic_templates"
2095
+ ],
2096
+ "provides": [],
2097
+ "internal_calls": [],
2098
+ "enforcement": {
2099
+ "level": "none",
2100
+ "rules": []
2101
+ },
2102
+ "triggers_after": []
2103
+ },
2104
+ "nexo_schema_abstraction_retire": {
2105
+ "description": "Retire a diagnostic template (lifecycle, guidance-only)",
2106
+ "category": "failure_prevention",
2107
+ "source": "plugin:schema_abstraction",
2108
+ "requires": [
2109
+ "diagnostic_templates"
2110
+ ],
2111
+ "provides": [
2112
+ "diagnostic_templates"
2113
+ ],
2114
+ "internal_calls": [],
2115
+ "enforcement": {
2116
+ "level": "none",
2117
+ "rules": []
2118
+ },
2119
+ "triggers_after": []
2120
+ },
2055
2121
  "nexo_goal_engine_status": {
2056
2122
  "description": "Goal Engine telemetry readiness",
2057
2123
  "category": "goal",