nexo-brain 0.3.3 → 0.3.4
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/package.json +1 -1
- package/src/db.py +146 -61
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO — Cognitive co-operator for Claude Code. Atkinson-Shiffrin memory, semantic RAG, trust scoring, and metacognitive error prevention.",
|
|
6
6
|
"bin": {
|
package/src/db.py
CHANGED
|
@@ -241,17 +241,6 @@ def init_db():
|
|
|
241
241
|
user_signals TEXT,
|
|
242
242
|
summary TEXT NOT NULL
|
|
243
243
|
);
|
|
244
|
-
CREATE TABLE IF NOT EXISTS session_diary_draft (
|
|
245
|
-
sid TEXT PRIMARY KEY,
|
|
246
|
-
summary_draft TEXT DEFAULT '',
|
|
247
|
-
tasks_seen TEXT DEFAULT '[]',
|
|
248
|
-
change_ids TEXT DEFAULT '[]',
|
|
249
|
-
decision_ids TEXT DEFAULT '[]',
|
|
250
|
-
last_context_hint TEXT DEFAULT '',
|
|
251
|
-
heartbeat_count INTEGER DEFAULT 0,
|
|
252
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
253
|
-
updated_at TEXT DEFAULT (datetime('now'))
|
|
254
|
-
);
|
|
255
244
|
|
|
256
245
|
CREATE TABLE IF NOT EXISTS evolution_metrics (
|
|
257
246
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -279,55 +268,8 @@ def init_db():
|
|
|
279
268
|
""")
|
|
280
269
|
# foreign_keys=ON is set in get_db() per-connection
|
|
281
270
|
|
|
282
|
-
# ──
|
|
283
|
-
|
|
284
|
-
_migrate_add_column(conn, "learnings", "prevention", "TEXT DEFAULT ''")
|
|
285
|
-
_migrate_add_column(conn, "learnings", "applies_to", "TEXT DEFAULT ''")
|
|
286
|
-
_migrate_add_column(conn, "learnings", "status", "TEXT DEFAULT 'active'")
|
|
287
|
-
_migrate_add_column(conn, "learnings", "review_due_at", "REAL")
|
|
288
|
-
_migrate_add_column(conn, "learnings", "last_reviewed_at", "REAL")
|
|
289
|
-
_migrate_add_column(conn, "followups", "reasoning", "TEXT")
|
|
290
|
-
_migrate_add_column(conn, "task_history", "reasoning", "TEXT")
|
|
291
|
-
_migrate_add_column(conn, "decisions", "status", "TEXT DEFAULT 'pending_review'")
|
|
292
|
-
_migrate_add_column(conn, "decisions", "review_due_at", "TEXT")
|
|
293
|
-
_migrate_add_column(conn, "decisions", "last_reviewed_at", "TEXT")
|
|
294
|
-
_migrate_add_index(conn, "idx_decisions_domain", "decisions", "domain")
|
|
295
|
-
_migrate_add_index(conn, "idx_decisions_created", "decisions", "created_at")
|
|
296
|
-
_migrate_add_index(conn, "idx_decisions_review_due", "decisions", "review_due_at")
|
|
297
|
-
_migrate_add_index(conn, "idx_session_diary_sid", "session_diary", "session_id")
|
|
298
|
-
_migrate_add_column(conn, "session_diary", "mental_state", "TEXT")
|
|
299
|
-
_migrate_add_column(conn, "session_diary", "domain", "TEXT")
|
|
300
|
-
_migrate_add_column(conn, "session_diary", "user_signals", "TEXT")
|
|
301
|
-
_migrate_add_column(conn, "session_diary", "self_critique", "TEXT")
|
|
302
|
-
_migrate_add_column(conn, "session_diary", "source", "TEXT DEFAULT 'claude'")
|
|
303
|
-
_migrate_add_index(conn, "idx_change_log_created", "change_log", "created_at")
|
|
304
|
-
_migrate_add_index(conn, "idx_change_log_files", "change_log", "files")
|
|
305
|
-
_migrate_add_index(conn, "idx_learnings_status", "learnings", "status")
|
|
306
|
-
_migrate_add_index(conn, "idx_learnings_review_due", "learnings", "review_due_at")
|
|
307
|
-
|
|
308
|
-
conn.execute("""
|
|
309
|
-
CREATE TABLE IF NOT EXISTS error_repetitions (
|
|
310
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
311
|
-
new_learning_id INTEGER NOT NULL,
|
|
312
|
-
original_learning_id INTEGER NOT NULL,
|
|
313
|
-
similarity REAL NOT NULL,
|
|
314
|
-
area TEXT NOT NULL,
|
|
315
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
316
|
-
)
|
|
317
|
-
""")
|
|
318
|
-
conn.execute("""
|
|
319
|
-
CREATE TABLE IF NOT EXISTS guard_checks (
|
|
320
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
321
|
-
session_id TEXT,
|
|
322
|
-
files TEXT,
|
|
323
|
-
area TEXT,
|
|
324
|
-
learnings_returned INTEGER DEFAULT 0,
|
|
325
|
-
blocking_rules_returned INTEGER DEFAULT 0,
|
|
326
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
327
|
-
)
|
|
328
|
-
""")
|
|
329
|
-
_migrate_add_index(conn, "idx_error_repetitions_area", "error_repetitions", "area")
|
|
330
|
-
_migrate_add_index(conn, "idx_guard_checks_session", "guard_checks", "session_id")
|
|
271
|
+
# ── Run formal migrations ────────────────────────────────────
|
|
272
|
+
run_migrations(conn)
|
|
331
273
|
|
|
332
274
|
# ── FTS5 unified search index ────────────────────────────────
|
|
333
275
|
conn.execute("""
|
|
@@ -779,6 +721,149 @@ def _migrate_add_index(conn, index_name: str, table: str, column: str):
|
|
|
779
721
|
conn.commit()
|
|
780
722
|
|
|
781
723
|
|
|
724
|
+
# ── Formal Migration System ─────────────────────────────────────
|
|
725
|
+
#
|
|
726
|
+
# Each migration is (version, name, callable). Migrations run once
|
|
727
|
+
# and are tracked in schema_migrations. The version number MUST be
|
|
728
|
+
# strictly increasing. Add new migrations at the end of the list.
|
|
729
|
+
#
|
|
730
|
+
# For users upgrading via npm/git, init_db() calls run_migrations()
|
|
731
|
+
# automatically — no manual steps needed.
|
|
732
|
+
|
|
733
|
+
def _m1_learnings_columns(conn):
|
|
734
|
+
_migrate_add_column(conn, "learnings", "reasoning", "TEXT")
|
|
735
|
+
_migrate_add_column(conn, "learnings", "prevention", "TEXT DEFAULT ''")
|
|
736
|
+
_migrate_add_column(conn, "learnings", "applies_to", "TEXT DEFAULT ''")
|
|
737
|
+
_migrate_add_column(conn, "learnings", "status", "TEXT DEFAULT 'active'")
|
|
738
|
+
_migrate_add_column(conn, "learnings", "review_due_at", "REAL")
|
|
739
|
+
_migrate_add_column(conn, "learnings", "last_reviewed_at", "REAL")
|
|
740
|
+
|
|
741
|
+
def _m2_followups_reasoning(conn):
|
|
742
|
+
_migrate_add_column(conn, "followups", "reasoning", "TEXT")
|
|
743
|
+
_migrate_add_column(conn, "task_history", "reasoning", "TEXT")
|
|
744
|
+
|
|
745
|
+
def _m3_decisions_review(conn):
|
|
746
|
+
_migrate_add_column(conn, "decisions", "status", "TEXT DEFAULT 'pending_review'")
|
|
747
|
+
_migrate_add_column(conn, "decisions", "review_due_at", "TEXT")
|
|
748
|
+
_migrate_add_column(conn, "decisions", "last_reviewed_at", "TEXT")
|
|
749
|
+
_migrate_add_index(conn, "idx_decisions_domain", "decisions", "domain")
|
|
750
|
+
_migrate_add_index(conn, "idx_decisions_created", "decisions", "created_at")
|
|
751
|
+
_migrate_add_index(conn, "idx_decisions_review_due", "decisions", "review_due_at")
|
|
752
|
+
|
|
753
|
+
def _m4_session_diary_columns(conn):
|
|
754
|
+
_migrate_add_index(conn, "idx_session_diary_sid", "session_diary", "session_id")
|
|
755
|
+
_migrate_add_column(conn, "session_diary", "mental_state", "TEXT")
|
|
756
|
+
_migrate_add_column(conn, "session_diary", "domain", "TEXT")
|
|
757
|
+
_migrate_add_column(conn, "session_diary", "user_signals", "TEXT")
|
|
758
|
+
_migrate_add_column(conn, "session_diary", "self_critique", "TEXT")
|
|
759
|
+
|
|
760
|
+
def _m5_change_log_indexes(conn):
|
|
761
|
+
_migrate_add_index(conn, "idx_change_log_created", "change_log", "created_at")
|
|
762
|
+
_migrate_add_index(conn, "idx_change_log_files", "change_log", "files")
|
|
763
|
+
_migrate_add_index(conn, "idx_learnings_status", "learnings", "status")
|
|
764
|
+
_migrate_add_index(conn, "idx_learnings_review_due", "learnings", "review_due_at")
|
|
765
|
+
|
|
766
|
+
def _m6_error_guard_tables(conn):
|
|
767
|
+
conn.execute("""
|
|
768
|
+
CREATE TABLE IF NOT EXISTS error_repetitions (
|
|
769
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
770
|
+
new_learning_id INTEGER NOT NULL,
|
|
771
|
+
original_learning_id INTEGER NOT NULL,
|
|
772
|
+
similarity REAL NOT NULL,
|
|
773
|
+
area TEXT NOT NULL,
|
|
774
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
775
|
+
)
|
|
776
|
+
""")
|
|
777
|
+
conn.execute("""
|
|
778
|
+
CREATE TABLE IF NOT EXISTS guard_checks (
|
|
779
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
780
|
+
session_id TEXT,
|
|
781
|
+
files TEXT,
|
|
782
|
+
area TEXT,
|
|
783
|
+
learnings_returned INTEGER DEFAULT 0,
|
|
784
|
+
blocking_rules_returned INTEGER DEFAULT 0,
|
|
785
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
786
|
+
)
|
|
787
|
+
""")
|
|
788
|
+
_migrate_add_index(conn, "idx_error_repetitions_area", "error_repetitions", "area")
|
|
789
|
+
_migrate_add_index(conn, "idx_guard_checks_session", "guard_checks", "session_id")
|
|
790
|
+
|
|
791
|
+
def _m7_diary_source_and_draft(conn):
|
|
792
|
+
_migrate_add_column(conn, "session_diary", "source", "TEXT DEFAULT 'claude'")
|
|
793
|
+
conn.execute("""
|
|
794
|
+
CREATE TABLE IF NOT EXISTS session_diary_draft (
|
|
795
|
+
sid TEXT PRIMARY KEY,
|
|
796
|
+
summary_draft TEXT DEFAULT '',
|
|
797
|
+
tasks_seen TEXT DEFAULT '[]',
|
|
798
|
+
change_ids TEXT DEFAULT '[]',
|
|
799
|
+
decision_ids TEXT DEFAULT '[]',
|
|
800
|
+
last_context_hint TEXT DEFAULT '',
|
|
801
|
+
heartbeat_count INTEGER DEFAULT 0,
|
|
802
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
803
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
804
|
+
)
|
|
805
|
+
""")
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
# Migration registry — APPEND ONLY, never reorder or delete
|
|
809
|
+
MIGRATIONS = [
|
|
810
|
+
(1, "learnings_columns", _m1_learnings_columns),
|
|
811
|
+
(2, "followups_reasoning", _m2_followups_reasoning),
|
|
812
|
+
(3, "decisions_review", _m3_decisions_review),
|
|
813
|
+
(4, "session_diary_columns", _m4_session_diary_columns),
|
|
814
|
+
(5, "change_log_indexes", _m5_change_log_indexes),
|
|
815
|
+
(6, "error_guard_tables", _m6_error_guard_tables),
|
|
816
|
+
(7, "diary_source_and_draft", _m7_diary_source_and_draft),
|
|
817
|
+
]
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def run_migrations(conn=None):
|
|
821
|
+
"""Run pending migrations. Tracks applied versions in schema_migrations.
|
|
822
|
+
|
|
823
|
+
Safe to call multiple times — skips already-applied migrations.
|
|
824
|
+
Called automatically by init_db() on every server start.
|
|
825
|
+
"""
|
|
826
|
+
if conn is None:
|
|
827
|
+
conn = get_db()
|
|
828
|
+
|
|
829
|
+
conn.execute("""
|
|
830
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
831
|
+
version INTEGER PRIMARY KEY,
|
|
832
|
+
name TEXT NOT NULL,
|
|
833
|
+
applied_at TEXT DEFAULT (datetime('now'))
|
|
834
|
+
)
|
|
835
|
+
""")
|
|
836
|
+
conn.commit()
|
|
837
|
+
|
|
838
|
+
applied = {r[0] for r in conn.execute("SELECT version FROM schema_migrations").fetchall()}
|
|
839
|
+
|
|
840
|
+
for version, name, fn in MIGRATIONS:
|
|
841
|
+
if version not in applied:
|
|
842
|
+
try:
|
|
843
|
+
fn(conn)
|
|
844
|
+
conn.execute(
|
|
845
|
+
"INSERT INTO schema_migrations (version, name) VALUES (?, ?)",
|
|
846
|
+
(version, name)
|
|
847
|
+
)
|
|
848
|
+
conn.commit()
|
|
849
|
+
except Exception as e:
|
|
850
|
+
# Log but don't crash — partial migration is better than no server
|
|
851
|
+
import sys
|
|
852
|
+
print(f"[MIGRATION] v{version} ({name}) failed: {e}", file=sys.stderr)
|
|
853
|
+
|
|
854
|
+
return len(MIGRATIONS) - len(applied)
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def get_schema_version() -> int:
|
|
858
|
+
"""Return the highest applied migration version, or 0 if none."""
|
|
859
|
+
conn = get_db()
|
|
860
|
+
try:
|
|
861
|
+
row = conn.execute("SELECT MAX(version) FROM schema_migrations").fetchone()
|
|
862
|
+
return row[0] or 0
|
|
863
|
+
except Exception:
|
|
864
|
+
return 0
|
|
865
|
+
|
|
866
|
+
|
|
782
867
|
def _gen_id(prefix: str, length: int = 8) -> str:
|
|
783
868
|
"""Generate a random ID like 'msg-a1b2c3' or 'q-x9y8z7w6'."""
|
|
784
869
|
chars = string.ascii_lowercase + string.digits
|
|
@@ -2165,7 +2250,7 @@ def read_session_diary(session_id: str = '', last_n: int = 3, last_day: bool = F
|
|
|
2165
2250
|
- session_id: returns entries for that specific session
|
|
2166
2251
|
- last_day: returns ALL entries from the most recent day (multi-terminal aware)
|
|
2167
2252
|
- last_n: returns last N entries (default)
|
|
2168
|
-
- domain: filter by project context (
|
|
2253
|
+
- domain: filter by project context (nexo, other)
|
|
2169
2254
|
"""
|
|
2170
2255
|
conn = get_db()
|
|
2171
2256
|
domain_clause = " AND domain = ?" if domain else ""
|