nexo-brain 7.32.0 → 7.34.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/consolidation_prep.py +380 -0
- package/src/db/__init__.py +5 -1
- package/src/db/_episodic.py +32 -0
- package/src/db/_memory_v2.py +276 -0
- package/src/db/_protocol.py +35 -0
- package/src/db/_schema.py +207 -0
- package/src/hooks/auto_capture.py +60 -24
- package/src/learning_resolver.py +42 -0
- package/src/local_context/api.py +237 -33
- package/src/local_context/db.py +3 -2
- package/src/local_context/usage_events.py +2 -0
- package/src/memory_retrieval.py +96 -7
- package/src/message_batch_preview.py +290 -0
- package/src/plugins/protocol.py +218 -27
- package/src/ppr.py +473 -0
- package/src/pre_answer_router.py +316 -3
- package/src/pre_answer_runtime.py +156 -1
- package/src/resolution_cache.py +1119 -0
- package/src/scripts/deep-sleep/apply_findings.py +86 -9
- package/src/scripts/deep-sleep/rewrite.py +625 -0
- package/src/scripts/nexo-deep-sleep.sh +10 -0
- package/src/scripts/nexo-followup-runner.py +110 -8
- package/src/scripts/nexo-morning-agent.py +43 -2
- package/src/scripts/nexo-postmortem-consolidator.py +44 -1
- package/src/self_error_detector.py +414 -0
- package/src/semantic_layers.py +30 -3
- package/templates/core-prompts/morning-agent.md +3 -0
- package/templates/core-prompts/postmortem-consolidator.md +29 -2
|
@@ -221,17 +221,65 @@ def load_recent_dedupe_keys(target_date: str, days: int = 7) -> set[str]:
|
|
|
221
221
|
return keys
|
|
222
222
|
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
"""
|
|
224
|
+
class BackupError(RuntimeError):
|
|
225
|
+
"""Raised when a fail-closed backup cannot be produced."""
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def backup_db(db_path: Path, run_id: str, *, fail_closed: bool = False) -> Path | None:
|
|
229
|
+
"""Create a consistent snapshot of a database before mutations.
|
|
230
|
+
|
|
231
|
+
Uses the SQLite online backup API (``conn.backup``) so the snapshot is
|
|
232
|
+
transaction-consistent and folds in any pending ``-wal``/``-shm`` state.
|
|
233
|
+
The previous implementation used ``shutil.copy2`` of only the ``.db`` file,
|
|
234
|
+
which could capture a torn copy when a WAL sidecar was live and silently
|
|
235
|
+
swallowed failures while still mutating the DB afterwards.
|
|
236
|
+
|
|
237
|
+
fail_closed=True makes the caller's intent explicit: if a consistent
|
|
238
|
+
snapshot cannot be written, raise ``BackupError`` instead of returning
|
|
239
|
+
``None``. Callers that mutate memory MUST pass fail_closed=True so the
|
|
240
|
+
mutation phase aborts when no recovery point exists. Anti-data-loss
|
|
241
|
+
invariant: never mutate without a verified backup.
|
|
242
|
+
"""
|
|
226
243
|
if not db_path.exists():
|
|
244
|
+
if fail_closed:
|
|
245
|
+
# No source DB means nothing to back up and nothing to mutate; that
|
|
246
|
+
# is a safe no-op, not a backup failure.
|
|
247
|
+
return None
|
|
227
248
|
return None
|
|
228
|
-
backup_path = BACKUP_DIR / f"{run_id}-backup-{db_path.name}"
|
|
249
|
+
backup_path = Path(BACKUP_DIR) / f"{run_id}-backup-{db_path.name}"
|
|
229
250
|
try:
|
|
230
|
-
|
|
231
|
-
|
|
251
|
+
backup_path.parent.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
# Online backup API: consistent snapshot even with a live WAL.
|
|
253
|
+
src = sqlite3.connect(str(db_path))
|
|
254
|
+
try:
|
|
255
|
+
dst = sqlite3.connect(str(backup_path))
|
|
256
|
+
try:
|
|
257
|
+
src.backup(dst)
|
|
258
|
+
finally:
|
|
259
|
+
dst.close()
|
|
260
|
+
finally:
|
|
261
|
+
src.close()
|
|
262
|
+
# Verify the snapshot is a readable SQLite DB before trusting it.
|
|
263
|
+
verify = sqlite3.connect(str(backup_path))
|
|
264
|
+
try:
|
|
265
|
+
verify.execute("PRAGMA schema_version").fetchone()
|
|
266
|
+
finally:
|
|
267
|
+
verify.close()
|
|
268
|
+
if not backup_path.exists() or backup_path.stat().st_size == 0:
|
|
269
|
+
raise BackupError(f"backup snapshot empty for {db_path.name}")
|
|
232
270
|
return backup_path
|
|
233
271
|
except Exception as e:
|
|
234
|
-
|
|
272
|
+
# Best-effort cleanup of a partial/torn snapshot.
|
|
273
|
+
try:
|
|
274
|
+
if backup_path.exists():
|
|
275
|
+
backup_path.unlink()
|
|
276
|
+
except Exception:
|
|
277
|
+
pass
|
|
278
|
+
msg = f"backup failed for {db_path.name}: {e}"
|
|
279
|
+
if fail_closed:
|
|
280
|
+
print(f" [apply] ABORT: {msg}", file=sys.stderr)
|
|
281
|
+
raise BackupError(msg) from e
|
|
282
|
+
print(f" [apply] Warning: {msg}", file=sys.stderr)
|
|
235
283
|
return None
|
|
236
284
|
|
|
237
285
|
|
|
@@ -534,10 +582,35 @@ def _update_learning_row(learning_id: int, updates: dict[str, object]) -> None:
|
|
|
534
582
|
if not updates:
|
|
535
583
|
return
|
|
536
584
|
conn = sqlite3.connect(str(NEXO_DB))
|
|
585
|
+
conn.row_factory = sqlite3.Row
|
|
586
|
+
# Capture before-state for the changed columns so the edit is auditable and
|
|
587
|
+
# reversible (item_history is append-only; nothing is overwritten).
|
|
588
|
+
before: dict[str, object] = {}
|
|
589
|
+
try:
|
|
590
|
+
cols = ", ".join(updates.keys())
|
|
591
|
+
prev = conn.execute(
|
|
592
|
+
f"SELECT {cols} FROM learnings WHERE id = ?", (learning_id,)
|
|
593
|
+
).fetchone()
|
|
594
|
+
if prev is not None:
|
|
595
|
+
before = {k: prev[k] for k in updates.keys()}
|
|
596
|
+
except Exception:
|
|
597
|
+
before = {}
|
|
537
598
|
set_clause = ", ".join(f"{column} = ?" for column in updates)
|
|
538
599
|
conn.execute(f"UPDATE learnings SET {set_clause} WHERE id = ?", list(updates.values()) + [learning_id])
|
|
539
600
|
conn.commit()
|
|
540
601
|
conn.close()
|
|
602
|
+
# Append an immutable history event. Failure here must never break the edit.
|
|
603
|
+
try:
|
|
604
|
+
nexo_db.add_item_history(
|
|
605
|
+
"learning",
|
|
606
|
+
str(learning_id),
|
|
607
|
+
"deep_sleep_update",
|
|
608
|
+
note="Deep Sleep edited learning fields",
|
|
609
|
+
actor="deep_sleep",
|
|
610
|
+
metadata={"before": before, "after": updates, "primitive": "_update_learning_row"},
|
|
611
|
+
)
|
|
612
|
+
except Exception:
|
|
613
|
+
pass
|
|
541
614
|
|
|
542
615
|
|
|
543
616
|
def _bump_weight(existing_value, amount: float) -> float:
|
|
@@ -2351,12 +2424,16 @@ def main():
|
|
|
2351
2424
|
existing_keys = load_recent_dedupe_keys(target_date)
|
|
2352
2425
|
print(f"[apply] Existing dedupe keys (7d): {len(existing_keys)}")
|
|
2353
2426
|
|
|
2354
|
-
# Backup databases before mutations
|
|
2427
|
+
# Backup databases before mutations (fail-closed: abort if no recovery point)
|
|
2355
2428
|
auto_apply_count = sum(1 for a in actions if a.get("action_class") == "auto_apply")
|
|
2356
2429
|
if auto_apply_count > 0:
|
|
2357
2430
|
print("[apply] Creating database backups...")
|
|
2358
|
-
|
|
2359
|
-
|
|
2431
|
+
try:
|
|
2432
|
+
nexo_backup = backup_db(NEXO_DB, run_id, fail_closed=True)
|
|
2433
|
+
cog_backup = backup_db(COGNITIVE_DB, run_id, fail_closed=True)
|
|
2434
|
+
except BackupError as e:
|
|
2435
|
+
print(f"[apply] ABORT: cannot mutate without a backup ({e}).", file=sys.stderr)
|
|
2436
|
+
sys.exit(1)
|
|
2360
2437
|
if nexo_backup:
|
|
2361
2438
|
print(f" Backup: {nexo_backup}")
|
|
2362
2439
|
if cog_backup:
|