nexo-brain 1.6.0 → 2.0.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/README.md +102 -79
- package/bin/nexo-brain.js +681 -303
- package/bin/postinstall.js +46 -0
- package/package.json +14 -2
- package/scripts/migrate-to-unified.sh +813 -0
- package/scripts/migrate-v1.7-to-v1.8.py +214 -0
- package/scripts/pre-commit-check.sh +1 -1
- package/src/__pycache__/auto_close_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-314.pyc +0 -0
- package/src/__pycache__/claim_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/claim_graph.cpython-314.pyc +0 -0
- package/src/__pycache__/evolution_cycle.cpython-310.pyc +0 -0
- package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
- package/src/__pycache__/maintenance.cpython-310.pyc +0 -0
- package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
- package/src/__pycache__/migrate_embeddings.cpython-310.pyc +0 -0
- package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/server.cpython-310.pyc +0 -0
- package/src/__pycache__/server.cpython-314.pyc +0 -0
- package/src/__pycache__/storage_router.cpython-310.pyc +0 -0
- package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
- package/src/auto_close_sessions.py +4 -3
- package/src/auto_update.py +634 -0
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/cognitive/_core.py +7 -3
- package/src/cognitive/_decay.py +1 -1
- package/src/cognitive/_memory.py +7 -3
- package/src/cognitive/_search.py +12 -10
- package/src/cognitive/_trust.py +3 -3
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
- package/src/dashboard/app.py +9 -3
- package/src/dashboard/templates/dashboard.html +4 -4
- package/src/dashboard/templates/operations.html +6 -6
- package/src/db/__init__.py +2 -1
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core.py +7 -3
- package/src/db/_episodic.py +69 -1
- package/src/db/_fts.py +12 -12
- package/src/db/_reminders.py +89 -15
- package/src/db/_schema.py +41 -0
- package/src/evolution_cycle.py +33 -11
- package/src/hooks/__pycache__/auto_capture.cpython-310.pyc +0 -0
- package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
- package/src/hooks/auto_capture.py +1 -1
- package/src/hooks/capture-tool-logs.sh +76 -0
- package/src/hooks/inbox-hook.sh +2 -1
- package/src/hooks/post-compact.sh +2 -1
- package/src/hooks/pre-compact.sh +104 -2
- package/src/hooks/session-start.sh +6 -2
- package/src/hooks/session-stop.sh +2 -1
- package/src/kg_populate.py +4 -1
- package/src/migrate_embeddings.py +4 -1
- package/src/plugin_loader.py +100 -34
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/agents.py +2 -2
- package/src/plugins/backup.py +5 -4
- package/src/plugins/cognitive_memory.py +1 -1
- package/src/plugins/core_rules.py +5 -1
- package/src/plugins/episodic_memory.py +43 -16
- package/src/plugins/evolution.py +7 -2
- package/src/plugins/guard.py +45 -17
- package/src/plugins/update.py +238 -0
- package/src/requirements.txt +12 -0
- package/src/rules/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/rules/__pycache__/migrate.cpython-310.pyc +0 -0
- package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/check-context.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-github-monitor.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-github-monitor.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/check-context.py +13 -3
- package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-310.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-310.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-310.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/analyze_session.py +3 -1
- package/src/scripts/deep-sleep/apply_findings.py +7 -4
- package/src/scripts/deep-sleep/collect_transcripts.py +3 -1
- package/src/scripts/nexo-auto-update.py +4 -211
- package/src/scripts/nexo-backup.sh +25 -0
- package/src/scripts/nexo-brain-activation.sh +26 -26
- package/src/scripts/nexo-catchup.py +39 -25
- package/src/scripts/nexo-cognitive-decay.py +12 -5
- package/src/scripts/nexo-daily-self-audit.py +36 -14
- package/src/scripts/nexo-deep-sleep.sh +4 -3
- package/src/scripts/nexo-evolution-run.py +40 -12
- package/src/scripts/nexo-followup-hygiene.py +6 -3
- package/src/scripts/nexo-github-monitor.py +11 -4
- package/src/scripts/nexo-immune.py +20 -3
- package/src/scripts/nexo-inbox-hook.sh +2 -1
- package/src/scripts/nexo-install.py +6 -0
- package/src/scripts/nexo-learning-housekeep.py +245 -0
- package/src/scripts/nexo-learning-validator.py +12 -2
- package/src/scripts/nexo-migrate.py +232 -0
- package/src/scripts/nexo-postmortem-consolidator.py +38 -19
- package/src/scripts/nexo-pre-commit.py +3 -1
- package/src/scripts/nexo-prevent-sleep.sh +29 -0
- package/src/scripts/nexo-proactive-dashboard.py +8 -6
- package/src/scripts/nexo-runtime-preflight.py +59 -55
- package/src/scripts/nexo-send-email.py +2 -2
- package/src/scripts/nexo-send-reply.py +3 -1
- package/src/scripts/nexo-sleep.py +21 -5
- package/src/scripts/nexo-snapshot-restore.sh +2 -1
- package/src/scripts/nexo-synthesis.py +19 -4
- package/src/scripts/nexo-tcc-approve.sh +79 -0
- package/src/scripts/nexo-update.sh +161 -0
- package/src/scripts/nexo-watchdog-smoke.py +18 -13
- package/src/scripts/nexo-watchdog.sh +41 -31
- package/src/server.py +107 -44
- package/src/storage_router.py +6 -2
- package/src/tools_coordination.py +14 -14
- package/src/tools_credentials.py +11 -11
- package/src/tools_learnings.py +36 -27
- package/src/tools_menu.py +7 -6
- package/src/tools_reminders.py +11 -5
- package/src/tools_reminders_crud.py +11 -9
- package/src/tools_sessions.py +62 -187
- package/src/tools_task_history.py +7 -7
- package/templates/CLAUDE.md.template +49 -469
- package/templates/launchagents/README.md +7 -7
- package/templates/launchagents/com.nexo.auto-close-sessions.plist +5 -1
- package/templates/launchagents/com.nexo.catchup.plist +4 -0
- package/templates/launchagents/com.nexo.cognitive-decay.plist +7 -0
- package/templates/launchagents/com.nexo.dashboard.plist +5 -1
- package/templates/launchagents/com.nexo.deep-sleep.plist +4 -0
- package/templates/launchagents/com.nexo.evolution.plist +4 -0
- package/templates/launchagents/com.nexo.followup-hygiene.plist +4 -0
- package/templates/launchagents/com.nexo.github-monitor.plist +3 -1
- package/templates/launchagents/com.nexo.immune.plist +4 -0
- package/templates/launchagents/com.nexo.postmortem.plist +4 -0
- package/templates/launchagents/com.nexo.self-audit.plist +4 -0
- package/templates/launchagents/com.nexo.synthesis.plist +4 -0
- package/templates/launchagents/com.nexo.watchdog.plist +4 -0
- package/templates/openclaw.json +1 -1
- package/tests/__pycache__/__init__.cpython-310.pyc +0 -0
- package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tests/__pycache__/conftest.cpython-310-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/conftest.cpython-310.pyc +0 -0
- package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_cognitive.cpython-310-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_cognitive.cpython-310.pyc +0 -0
- package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_knowledge_graph.cpython-310-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_knowledge_graph.cpython-310.pyc +0 -0
- package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_migrations.cpython-310-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_migrations.cpython-310.pyc +0 -0
- package/tests/__pycache__/test_migrations.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/conftest.py +2 -2
- package/tests/test_cognitive.py +7 -6
- package/tests/test_migrations.py +29 -3
package/src/db/_reminders.py
CHANGED
|
@@ -7,7 +7,7 @@ from db._fts import fts_upsert
|
|
|
7
7
|
# ── Reminders ──────────────────────────────────────────────────────
|
|
8
8
|
|
|
9
9
|
def create_reminder(id: str, description: str, date: str = None,
|
|
10
|
-
status: str = '
|
|
10
|
+
status: str = 'PENDING', category: str = 'general') -> dict:
|
|
11
11
|
"""Create a new reminder."""
|
|
12
12
|
conn = get_db()
|
|
13
13
|
now = now_epoch()
|
|
@@ -46,7 +46,7 @@ def update_reminder(id: str, **kwargs) -> dict:
|
|
|
46
46
|
def complete_reminder(id: str) -> dict:
|
|
47
47
|
"""Mark a reminder as completed with today's date."""
|
|
48
48
|
today = datetime.date.today().isoformat()
|
|
49
|
-
return update_reminder(id, status="
|
|
49
|
+
return update_reminder(id, status="COMPLETED")
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def delete_reminder(id: str) -> bool:
|
|
@@ -64,19 +64,19 @@ def get_reminders(filter_type: str = 'all') -> list[dict]:
|
|
|
64
64
|
today = datetime.date.today().isoformat()
|
|
65
65
|
if filter_type == 'completed':
|
|
66
66
|
rows = conn.execute(
|
|
67
|
-
"SELECT * FROM reminders WHERE status LIKE '
|
|
67
|
+
"SELECT * FROM reminders WHERE status LIKE 'COMPLETED%' ORDER BY updated_at DESC"
|
|
68
68
|
).fetchall()
|
|
69
69
|
elif filter_type == 'due':
|
|
70
70
|
rows = conn.execute(
|
|
71
|
-
"SELECT * FROM reminders WHERE status NOT LIKE '
|
|
72
|
-
"AND status != '
|
|
71
|
+
"SELECT * FROM reminders WHERE status NOT LIKE 'COMPLETED%' "
|
|
72
|
+
"AND status != 'DELETED' AND date IS NOT NULL AND date <= ? "
|
|
73
73
|
"ORDER BY date ASC",
|
|
74
74
|
(today,)
|
|
75
75
|
).fetchall()
|
|
76
76
|
else: # 'all' — active only
|
|
77
77
|
rows = conn.execute(
|
|
78
|
-
"SELECT * FROM reminders WHERE status NOT LIKE '
|
|
79
|
-
"AND status != '
|
|
78
|
+
"SELECT * FROM reminders WHERE status NOT LIKE 'COMPLETED%' "
|
|
79
|
+
"AND status != 'DELETED' ORDER BY date ASC NULLS LAST"
|
|
80
80
|
).fetchall()
|
|
81
81
|
return [dict(r) for r in rows]
|
|
82
82
|
|
|
@@ -88,18 +88,68 @@ def get_reminder(id: str) -> dict | None:
|
|
|
88
88
|
return dict(row) if row else None
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
def find_similar_followups(description: str, threshold: float = 0.3) -> list[dict]:
|
|
92
|
+
"""Find open followups similar to a description using keyword overlap.
|
|
93
|
+
|
|
94
|
+
Uses asymmetric scoring: what fraction of the SMALLER token set overlaps
|
|
95
|
+
with the larger. This handles different-length texts better than Jaccard.
|
|
96
|
+
|
|
97
|
+
Returns matches sorted by similarity score (highest first).
|
|
98
|
+
threshold: minimum overlap ratio (0.0-1.0) to consider a match.
|
|
99
|
+
"""
|
|
100
|
+
conn = get_db()
|
|
101
|
+
rows = conn.execute(
|
|
102
|
+
"SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
|
|
103
|
+
"AND status != 'DELETED'"
|
|
104
|
+
).fetchall()
|
|
105
|
+
|
|
106
|
+
def tokenize(text: str) -> set:
|
|
107
|
+
return {w.lower() for w in text.split() if len(w) > 3}
|
|
108
|
+
|
|
109
|
+
query_tokens = tokenize(description)
|
|
110
|
+
if not query_tokens:
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
matches = []
|
|
114
|
+
for row in rows:
|
|
115
|
+
existing_tokens = tokenize(f"{row['id']} {row['description']} {row['verification'] or ''}")
|
|
116
|
+
if not existing_tokens:
|
|
117
|
+
continue
|
|
118
|
+
intersection = query_tokens & existing_tokens
|
|
119
|
+
if not intersection:
|
|
120
|
+
continue
|
|
121
|
+
smaller = min(len(query_tokens), len(existing_tokens))
|
|
122
|
+
score = len(intersection) / smaller if smaller else 0
|
|
123
|
+
if score >= threshold:
|
|
124
|
+
matches.append({**dict(row), "_similarity": round(score, 2)})
|
|
125
|
+
|
|
126
|
+
matches.sort(key=lambda x: x["_similarity"], reverse=True)
|
|
127
|
+
return matches[:5]
|
|
128
|
+
|
|
129
|
+
|
|
91
130
|
# ── Followups ──────────────────────────────────────────────────────
|
|
92
131
|
|
|
93
132
|
def create_followup(id: str, description: str, date: str = None,
|
|
94
|
-
verification: str = '', status: str = '
|
|
133
|
+
verification: str = '', status: str = 'PENDING',
|
|
95
134
|
reasoning: str = '', recurrence: str = None) -> dict:
|
|
96
135
|
"""Create a new followup with optional reasoning and recurrence.
|
|
97
136
|
|
|
137
|
+
Checks for similar open followups before creating. If a match is found,
|
|
138
|
+
returns a warning with the existing followup ID (still creates the new one).
|
|
139
|
+
|
|
98
140
|
recurrence format: 'weekly:monday', 'monthly:1', 'monthly:10', 'quarterly', etc.
|
|
99
141
|
When a recurring followup is completed, a new one is auto-created with the next date.
|
|
100
142
|
"""
|
|
101
143
|
conn = get_db()
|
|
102
144
|
now = now_epoch()
|
|
145
|
+
|
|
146
|
+
# Anti-duplicate check
|
|
147
|
+
similar = find_similar_followups(description)
|
|
148
|
+
warning = ""
|
|
149
|
+
if similar:
|
|
150
|
+
ids = ", ".join(s["id"] for s in similar[:3])
|
|
151
|
+
warning = f" ⚠ SIMILAR FOLLOWUPS EXIST: {ids} (scores: {', '.join(str(s['_similarity']) for s in similar[:3])}). Consider updating instead."
|
|
152
|
+
|
|
103
153
|
try:
|
|
104
154
|
conn.execute(
|
|
105
155
|
"INSERT INTO followups (id, date, description, verification, status, reasoning, recurrence, created_at, updated_at) "
|
|
@@ -111,7 +161,10 @@ def create_followup(id: str, description: str, date: str = None,
|
|
|
111
161
|
except sqlite3.IntegrityError:
|
|
112
162
|
return {"error": f"Followup {id} already exists. Use update instead."}
|
|
113
163
|
row = conn.execute("SELECT * FROM followups WHERE id = ?", (id,)).fetchone()
|
|
114
|
-
|
|
164
|
+
result = dict(row)
|
|
165
|
+
if warning:
|
|
166
|
+
result["warning"] = warning
|
|
167
|
+
return result
|
|
115
168
|
|
|
116
169
|
|
|
117
170
|
def update_followup(id: str, **kwargs) -> dict:
|
|
@@ -190,7 +243,7 @@ def complete_followup(id: str, result: str = '') -> dict:
|
|
|
190
243
|
return {"error": f"Followup {id} not found"}
|
|
191
244
|
|
|
192
245
|
today = datetime.date.today().isoformat()
|
|
193
|
-
kwargs = {"status": "
|
|
246
|
+
kwargs = {"status": "COMPLETED"}
|
|
194
247
|
if result:
|
|
195
248
|
existing = row["verification"] or ''
|
|
196
249
|
kwargs["verification"] = f"{existing}\n{result}".strip() if existing else result
|
|
@@ -206,6 +259,18 @@ def complete_followup(id: str, result: str = '') -> dict:
|
|
|
206
259
|
archived_id = f"{id}-{today}"
|
|
207
260
|
conn.execute("UPDATE followups SET id = ? WHERE id = ?", (archived_id, id))
|
|
208
261
|
conn.commit()
|
|
262
|
+
|
|
263
|
+
# Fix FTS: remove old entry for original ID, add entry for archived ID
|
|
264
|
+
conn.execute("DELETE FROM unified_search WHERE source = 'followup' AND source_id = ?", (id,))
|
|
265
|
+
archived_row = conn.execute("SELECT * FROM followups WHERE id = ?", (archived_id,)).fetchone()
|
|
266
|
+
if archived_row:
|
|
267
|
+
fts_upsert(
|
|
268
|
+
"followup", archived_id, archived_id,
|
|
269
|
+
f"{archived_row['description']} {archived_row['verification'] or ''} {archived_row['reasoning'] or ''}",
|
|
270
|
+
"followup", commit=False,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# create_followup handles its own FTS entry for the new recurring ID
|
|
209
274
|
create_followup(
|
|
210
275
|
id=id,
|
|
211
276
|
description=row["description"],
|
|
@@ -215,6 +280,15 @@ def complete_followup(id: str, result: str = '') -> dict:
|
|
|
215
280
|
recurrence=recurrence,
|
|
216
281
|
)
|
|
217
282
|
|
|
283
|
+
# Return accurate result: the completed one is now archived_id, not id
|
|
284
|
+
return {
|
|
285
|
+
"id": archived_id,
|
|
286
|
+
"status": "COMPLETED",
|
|
287
|
+
"recurrence": recurrence,
|
|
288
|
+
"next_id": id,
|
|
289
|
+
"next_date": next_date,
|
|
290
|
+
}
|
|
291
|
+
|
|
218
292
|
return update_result
|
|
219
293
|
|
|
220
294
|
|
|
@@ -234,19 +308,19 @@ def get_followups(filter_type: str = 'all') -> list[dict]:
|
|
|
234
308
|
today = datetime.date.today().isoformat()
|
|
235
309
|
if filter_type == 'completed':
|
|
236
310
|
rows = conn.execute(
|
|
237
|
-
"SELECT * FROM followups WHERE status LIKE '
|
|
311
|
+
"SELECT * FROM followups WHERE status LIKE 'COMPLETED%' ORDER BY updated_at DESC"
|
|
238
312
|
).fetchall()
|
|
239
313
|
elif filter_type == 'due':
|
|
240
314
|
rows = conn.execute(
|
|
241
|
-
"SELECT * FROM followups WHERE status NOT LIKE '
|
|
242
|
-
"AND status != '
|
|
315
|
+
"SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
|
|
316
|
+
"AND status != 'DELETED' AND date IS NOT NULL AND date <= ? "
|
|
243
317
|
"ORDER BY date ASC",
|
|
244
318
|
(today,)
|
|
245
319
|
).fetchall()
|
|
246
320
|
else: # 'all' — active only
|
|
247
321
|
rows = conn.execute(
|
|
248
|
-
"SELECT * FROM followups WHERE status NOT LIKE '
|
|
249
|
-
"AND status != '
|
|
322
|
+
"SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
|
|
323
|
+
"AND status != 'DELETED' ORDER BY date ASC NULLS LAST"
|
|
250
324
|
).fetchall()
|
|
251
325
|
return [dict(r) for r in rows]
|
|
252
326
|
|
package/src/db/_schema.py
CHANGED
|
@@ -256,6 +256,45 @@ def _m13_claude_session_id(conn):
|
|
|
256
256
|
conn.commit()
|
|
257
257
|
|
|
258
258
|
|
|
259
|
+
def _m14_learnings_priority_weight(conn):
|
|
260
|
+
"""Add priority, weight, and guard usage tracking to learnings + followup priority."""
|
|
261
|
+
_migrate_add_column(conn, "learnings", "priority", "TEXT DEFAULT 'medium'")
|
|
262
|
+
_migrate_add_column(conn, "learnings", "weight", "REAL DEFAULT 0.5")
|
|
263
|
+
_migrate_add_column(conn, "learnings", "guard_hits", "INTEGER DEFAULT 0")
|
|
264
|
+
_migrate_add_column(conn, "learnings", "last_guard_hit_at", "REAL")
|
|
265
|
+
_migrate_add_column(conn, "followups", "priority", "TEXT DEFAULT 'medium'")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _m15_core_rules_tables(conn):
|
|
269
|
+
"""Core rules and version tracking tables for the core_rules plugin."""
|
|
270
|
+
conn.execute("""
|
|
271
|
+
CREATE TABLE IF NOT EXISTS core_rules (
|
|
272
|
+
id TEXT PRIMARY KEY,
|
|
273
|
+
category TEXT NOT NULL,
|
|
274
|
+
rule TEXT NOT NULL,
|
|
275
|
+
why TEXT NOT NULL,
|
|
276
|
+
importance INTEGER NOT NULL DEFAULT 3,
|
|
277
|
+
type TEXT NOT NULL DEFAULT 'advisory',
|
|
278
|
+
added_in TEXT DEFAULT '',
|
|
279
|
+
removed_in TEXT DEFAULT NULL,
|
|
280
|
+
is_active INTEGER NOT NULL DEFAULT 1
|
|
281
|
+
)
|
|
282
|
+
""")
|
|
283
|
+
conn.execute("""
|
|
284
|
+
CREATE TABLE IF NOT EXISTS core_rules_version (
|
|
285
|
+
id INTEGER PRIMARY KEY,
|
|
286
|
+
version TEXT NOT NULL DEFAULT '0.0.0',
|
|
287
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
288
|
+
)
|
|
289
|
+
""")
|
|
290
|
+
# Seed the version row so UPDATE statements in the plugin always find it
|
|
291
|
+
conn.execute(
|
|
292
|
+
"INSERT OR IGNORE INTO core_rules_version (id, version) VALUES (1, '0.0.0')"
|
|
293
|
+
)
|
|
294
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_core_rules_category ON core_rules(category)")
|
|
295
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_core_rules_active ON core_rules(is_active)")
|
|
296
|
+
|
|
297
|
+
|
|
259
298
|
# Migration registry — APPEND ONLY, never reorder or delete
|
|
260
299
|
MIGRATIONS = [
|
|
261
300
|
(1, "learnings_columns", _m1_learnings_columns),
|
|
@@ -271,6 +310,8 @@ MIGRATIONS = [
|
|
|
271
310
|
(11, "artifact_registry", _m11_artifact_registry),
|
|
272
311
|
(12, "session_checkpoints", _m12_session_checkpoints),
|
|
273
312
|
(13, "claude_session_id", _m13_claude_session_id),
|
|
313
|
+
(14, "learnings_priority_weight", _m14_learnings_priority_weight),
|
|
314
|
+
(15, "core_rules_tables", _m15_core_rules_tables),
|
|
274
315
|
]
|
|
275
316
|
|
|
276
317
|
|
package/src/evolution_cycle.py
CHANGED
|
@@ -14,14 +14,22 @@ import time
|
|
|
14
14
|
from datetime import datetime, date, timedelta
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
SANDBOX_DIR =
|
|
21
|
-
SNAPSHOTS_DIR =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
18
|
+
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(NEXO_HOME)))
|
|
19
|
+
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
20
|
+
SANDBOX_DIR = NEXO_HOME / "sandbox" / "workspace"
|
|
21
|
+
SNAPSHOTS_DIR = NEXO_HOME / "snapshots"
|
|
22
|
+
RESTORE_LOG = NEXO_HOME / "logs" / "snapshot-restores.log"
|
|
23
|
+
|
|
24
|
+
# Evolution config: brain/ (canonical) > cortex/ (legacy) > NEXO_CODE (dev)
|
|
25
|
+
def _resolve_evolution_file(name: str) -> Path:
|
|
26
|
+
for candidate in [NEXO_HOME / "brain" / name, NEXO_HOME / "cortex" / name, NEXO_CODE / name]:
|
|
27
|
+
if candidate.exists():
|
|
28
|
+
return candidate
|
|
29
|
+
return NEXO_HOME / "brain" / name # default canonical path
|
|
30
|
+
|
|
31
|
+
OBJECTIVE_FILE = _resolve_evolution_file("evolution-objective.json")
|
|
32
|
+
PROMPT_FILE = _resolve_evolution_file("evolution-prompt.md")
|
|
25
33
|
|
|
26
34
|
MAX_SNAPSHOTS = 8
|
|
27
35
|
|
|
@@ -105,6 +113,8 @@ def create_snapshot(files_to_backup: list) -> str:
|
|
|
105
113
|
rel = str(fp).replace(str(Path.home()) + "/", "")
|
|
106
114
|
dest = files_dir / rel
|
|
107
115
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
if os.path.abspath(str(fp)) == os.path.abspath(str(dest)):
|
|
117
|
+
continue # Skip: source and destination are the same file
|
|
108
118
|
shutil.copy2(fp, dest)
|
|
109
119
|
manifest["files"].append(rel)
|
|
110
120
|
|
|
@@ -144,9 +154,21 @@ def dry_run_restore_test() -> bool:
|
|
|
144
154
|
|
|
145
155
|
test_file.write_text("modified_content")
|
|
146
156
|
|
|
157
|
+
# Find restore script: NEXO_CODE/scripts/ first, then NEXO_HOME/scripts/
|
|
158
|
+
_nexo_code = Path(os.environ.get("NEXO_CODE", ""))
|
|
159
|
+
restore_script = None
|
|
160
|
+
for candidate in [_nexo_code / "scripts" / "nexo-snapshot-restore.sh",
|
|
161
|
+
NEXO_HOME / "scripts" / "nexo-snapshot-restore.sh"]:
|
|
162
|
+
if candidate.exists():
|
|
163
|
+
restore_script = candidate
|
|
164
|
+
break
|
|
165
|
+
if not restore_script:
|
|
166
|
+
test_file.unlink(missing_ok=True)
|
|
167
|
+
return False # No restore script available
|
|
168
|
+
|
|
147
169
|
try:
|
|
148
170
|
subprocess.run(
|
|
149
|
-
[str(
|
|
171
|
+
[str(restore_script), snap_dir],
|
|
150
172
|
capture_output=True, timeout=10, check=True
|
|
151
173
|
)
|
|
152
174
|
content = test_file.read_text()
|
|
@@ -200,7 +222,7 @@ INVESTIGATE using these tools:
|
|
|
200
222
|
4. Read ~/.nexo/coordination/postmortem-daily.md — self-critique patterns
|
|
201
223
|
5. Read ~/.nexo/logs/self-audit-summary.json — system health
|
|
202
224
|
6. Glob ~/.nexo/scripts/*.py — existing scripts
|
|
203
|
-
7. Glob ~/.nexo/
|
|
225
|
+
7. Glob ~/.nexo/plugins/*.py — existing plugins
|
|
204
226
|
|
|
205
227
|
LOOK FOR:
|
|
206
228
|
- Repeated errors that guard isn't preventing
|
|
@@ -210,7 +232,7 @@ LOOK FOR:
|
|
|
210
232
|
- Patterns in self-critique that suggest systemic issues
|
|
211
233
|
|
|
212
234
|
SAFETY:
|
|
213
|
-
- Safe zones for auto changes: ~/.nexo/scripts/, ~/.nexo/
|
|
235
|
+
- Safe zones for auto changes: ~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/
|
|
214
236
|
- IMMUTABLE files (never touch): db.py, server.py, plugin_loader.py, cognitive.py, CLAUDE.md
|
|
215
237
|
- Every change needs: what file, what to change, why, risk, how to verify
|
|
216
238
|
|
|
Binary file
|
|
Binary file
|
|
@@ -40,6 +40,82 @@ record = {
|
|
|
40
40
|
print(json.dumps(record))
|
|
41
41
|
" >> "$LOG_FILE" 2>/dev/null
|
|
42
42
|
|
|
43
|
+
# ── Layer 1: Auto-diary every 10 tool calls ─────────────────────────
|
|
44
|
+
COUNTER_FILE="$NEXO_HOME/operations/.tool-call-count"
|
|
45
|
+
NEXO_DB="$NEXO_HOME/data/nexo.db"
|
|
46
|
+
|
|
47
|
+
# Increment counter (atomic: read+write in one step)
|
|
48
|
+
COUNT=1
|
|
49
|
+
if [ -f "$COUNTER_FILE" ]; then
|
|
50
|
+
COUNT=$(( $(cat "$COUNTER_FILE" 2>/dev/null || echo 0) + 1 ))
|
|
51
|
+
fi
|
|
52
|
+
echo "$COUNT" > "$COUNTER_FILE"
|
|
53
|
+
|
|
54
|
+
# Every 10 tool calls, write a mechanical diary draft to SQLite
|
|
55
|
+
if [ $(( COUNT % 10 )) -eq 0 ] && [ -f "$NEXO_DB" ]; then
|
|
56
|
+
python3 -c "
|
|
57
|
+
import json, sqlite3, os, sys
|
|
58
|
+
from datetime import datetime
|
|
59
|
+
|
|
60
|
+
db_path = '$NEXO_DB'
|
|
61
|
+
log_file = '$LOG_FILE'
|
|
62
|
+
count = $COUNT
|
|
63
|
+
|
|
64
|
+
# Read last 10 tool calls from today's log
|
|
65
|
+
entries = []
|
|
66
|
+
if os.path.isfile(log_file):
|
|
67
|
+
with open(log_file, 'r') as f:
|
|
68
|
+
lines = f.readlines()
|
|
69
|
+
for line in lines[-10:]:
|
|
70
|
+
try:
|
|
71
|
+
e = json.loads(line.strip())
|
|
72
|
+
name = e.get('tool_name', '?')
|
|
73
|
+
inp = e.get('tool_input', {})
|
|
74
|
+
# Brief args: first key's value, truncated
|
|
75
|
+
brief = ''
|
|
76
|
+
if isinstance(inp, dict):
|
|
77
|
+
for k, v in list(inp.items())[:1]:
|
|
78
|
+
brief = str(v)[:60]
|
|
79
|
+
entries.append(f'{name}({brief})')
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
if not entries:
|
|
84
|
+
sys.exit(0)
|
|
85
|
+
|
|
86
|
+
tools_summary = ', '.join(entries[-10:])
|
|
87
|
+
|
|
88
|
+
# Get current session and task from sessions table
|
|
89
|
+
conn = sqlite3.connect(db_path, timeout=2)
|
|
90
|
+
conn.row_factory = sqlite3.Row
|
|
91
|
+
row = conn.execute(
|
|
92
|
+
'SELECT sid, task FROM sessions ORDER BY last_update_epoch DESC LIMIT 1'
|
|
93
|
+
).fetchone()
|
|
94
|
+
if not row:
|
|
95
|
+
conn.close()
|
|
96
|
+
sys.exit(0)
|
|
97
|
+
|
|
98
|
+
sid = row['sid']
|
|
99
|
+
task = row['task'] or 'unknown'
|
|
100
|
+
|
|
101
|
+
summary = f'[AUTO-{count}] {len(entries)} tool calls: {tools_summary[:250]}. Task: {task[:100]}'
|
|
102
|
+
|
|
103
|
+
# Write to session_diary_draft (UPSERT)
|
|
104
|
+
conn.execute('''
|
|
105
|
+
INSERT INTO session_diary_draft (sid, summary_draft, tasks_seen, change_ids, decision_ids, last_context_hint, heartbeat_count, updated_at)
|
|
106
|
+
VALUES (?, ?, '[]', '[]', '[]', ?, 0, datetime('now'))
|
|
107
|
+
ON CONFLICT(sid) DO UPDATE SET
|
|
108
|
+
summary_draft = excluded.summary_draft,
|
|
109
|
+
last_context_hint = excluded.last_context_hint,
|
|
110
|
+
updated_at = datetime('now')
|
|
111
|
+
''', (sid, summary, f'auto-diary at {count} tool calls'))
|
|
112
|
+
conn.commit()
|
|
113
|
+
conn.close()
|
|
114
|
+
" 2>/dev/null &
|
|
115
|
+
# Reset counter after writing
|
|
116
|
+
echo "0" > "$COUNTER_FILE"
|
|
117
|
+
fi
|
|
118
|
+
|
|
43
119
|
# Cleanup: delete logs >= 30 days old (once daily, uses marker file)
|
|
44
120
|
CLEANUP_MARKER="$LOG_DIR/.last-cleanup"
|
|
45
121
|
if [ ! -f "$CLEANUP_MARKER" ] || [ "$(cat "$CLEANUP_MARKER" 2>/dev/null)" != "$TODAY" ]; then
|
package/src/hooks/inbox-hook.sh
CHANGED
|
@@ -27,7 +27,8 @@ echo "$NOW" > "$DEBOUNCE_FILE"
|
|
|
27
27
|
|
|
28
28
|
# 4. Find NEXO SID mapped to this Claude session_id
|
|
29
29
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
30
|
-
DB="$NEXO_HOME/nexo.db"
|
|
30
|
+
DB="$NEXO_HOME/data/nexo.db"
|
|
31
|
+
mkdir -p "$NEXO_HOME/data"
|
|
31
32
|
[ -f "$DB" ] || exit 0
|
|
32
33
|
|
|
33
34
|
NEXO_SID=$(sqlite3 "$DB" "SELECT sid FROM sessions WHERE claude_session_id = '${CLAUDE_SID}' AND last_update_epoch > (strftime('%s','now') - 900) ORDER BY last_update_epoch DESC LIMIT 1;" 2>/dev/null)
|
package/src/hooks/pre-compact.sh
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
|
|
8
8
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
9
|
-
NEXO_DB="$NEXO_HOME/nexo.db"
|
|
9
|
+
NEXO_DB="$NEXO_HOME/data/nexo.db"
|
|
10
|
+
mkdir -p "$NEXO_HOME/data"
|
|
10
11
|
TODAY=$(date +%Y-%m-%d)
|
|
11
12
|
LOG_FILE="$NEXO_HOME/operations/tool-logs/${TODAY}.jsonl"
|
|
12
13
|
LOG_LINES=0
|
|
@@ -42,8 +43,109 @@ if [ -f "$NEXO_DB" ]; then
|
|
|
42
43
|
fi
|
|
43
44
|
fi
|
|
44
45
|
|
|
46
|
+
# ── Layer 2: Emergency auto-diary before compaction ──────────────────
|
|
47
|
+
# Write an actual session_diary entry (not draft) with mechanical summary
|
|
48
|
+
# This is the parachute — if the LLM never wrote a diary, at least this exists
|
|
49
|
+
if [ -f "$NEXO_DB" ]; then
|
|
50
|
+
python3 -c "
|
|
51
|
+
import json, sqlite3, os, sys
|
|
52
|
+
from datetime import datetime
|
|
53
|
+
|
|
54
|
+
db_path = '$NEXO_DB'
|
|
55
|
+
log_file = '$LOG_FILE'
|
|
56
|
+
|
|
57
|
+
conn = sqlite3.connect(db_path, timeout=3)
|
|
58
|
+
conn.row_factory = sqlite3.Row
|
|
59
|
+
|
|
60
|
+
# Get latest active session
|
|
61
|
+
row = conn.execute(
|
|
62
|
+
'SELECT sid, task FROM sessions ORDER BY last_update_epoch DESC LIMIT 1'
|
|
63
|
+
).fetchone()
|
|
64
|
+
if not row:
|
|
65
|
+
conn.close()
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
|
|
68
|
+
sid = row['sid']
|
|
69
|
+
task = row['task'] or 'unknown'
|
|
70
|
+
|
|
71
|
+
# Check if a real diary already exists for this session
|
|
72
|
+
has_diary = conn.execute(
|
|
73
|
+
'SELECT id FROM session_diary WHERE session_id = ? LIMIT 1', (sid,)
|
|
74
|
+
).fetchone()
|
|
75
|
+
if has_diary:
|
|
76
|
+
conn.close()
|
|
77
|
+
sys.exit(0) # LLM already wrote one, no need for emergency diary
|
|
78
|
+
|
|
79
|
+
# Find last diary timestamp to know where to start reading logs
|
|
80
|
+
last_diary = conn.execute(
|
|
81
|
+
'SELECT created_at FROM session_diary ORDER BY created_at DESC LIMIT 1'
|
|
82
|
+
).fetchone()
|
|
83
|
+
last_diary_ts = last_diary['created_at'] if last_diary else '1970-01-01T00:00:00Z'
|
|
84
|
+
|
|
85
|
+
# Read tool log entries since last diary
|
|
86
|
+
entries = []
|
|
87
|
+
modified_files = []
|
|
88
|
+
git_actions = []
|
|
89
|
+
if os.path.isfile(log_file):
|
|
90
|
+
with open(log_file, 'r') as f:
|
|
91
|
+
for line in f:
|
|
92
|
+
try:
|
|
93
|
+
e = json.loads(line.strip())
|
|
94
|
+
ts = e.get('timestamp', '')
|
|
95
|
+
if ts < last_diary_ts:
|
|
96
|
+
continue
|
|
97
|
+
name = e.get('tool_name', '?')
|
|
98
|
+
inp = e.get('tool_input', {}) or {}
|
|
99
|
+
brief = ''
|
|
100
|
+
if isinstance(inp, dict):
|
|
101
|
+
for k, v in list(inp.items())[:1]:
|
|
102
|
+
brief = str(v)[:80]
|
|
103
|
+
entries.append(f'{name}({brief})')
|
|
104
|
+
# Extract decisions from tool calls
|
|
105
|
+
if name in ('Edit', 'Write'):
|
|
106
|
+
fp = inp.get('file_path', inp.get('path', ''))
|
|
107
|
+
if fp:
|
|
108
|
+
modified_files.append(fp.split('/')[-1])
|
|
109
|
+
if name == 'Bash':
|
|
110
|
+
cmd = str(inp.get('command', ''))
|
|
111
|
+
if 'git commit' in cmd or 'git push' in cmd:
|
|
112
|
+
git_actions.append(cmd[:80])
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
if not entries:
|
|
117
|
+
conn.close()
|
|
118
|
+
sys.exit(0)
|
|
119
|
+
|
|
120
|
+
# Build mechanical diary
|
|
121
|
+
tools_summary = ', '.join(entries[-30:])[:500]
|
|
122
|
+
summary = f'[EMERGENCY PRE-COMPACT] {len(entries)} tool calls since last diary. Tools: {tools_summary}'
|
|
123
|
+
|
|
124
|
+
decisions = ''
|
|
125
|
+
if modified_files:
|
|
126
|
+
decisions = 'Modified: ' + ', '.join(set(modified_files))[:300]
|
|
127
|
+
if git_actions:
|
|
128
|
+
decisions += (' | Git: ' + ', '.join(git_actions))[:200]
|
|
129
|
+
if not decisions:
|
|
130
|
+
decisions = 'No file modifications detected in tool logs'
|
|
131
|
+
|
|
132
|
+
pending = f'Current task: {task[:200]}'
|
|
133
|
+
context_next = 'COMPACTION HAPPENED. Read this diary to continue. Check session_checkpoints and tool-logs for full context.'
|
|
134
|
+
|
|
135
|
+
# Write actual session_diary entry
|
|
136
|
+
conn.execute('''
|
|
137
|
+
INSERT INTO session_diary
|
|
138
|
+
(session_id, decisions, discarded, pending, context_next,
|
|
139
|
+
mental_state, domain, user_signals, summary, source)
|
|
140
|
+
VALUES (?, ?, '', ?, ?, 'auto-generated', 'auto', '', ?, 'pre-compact-hook')
|
|
141
|
+
''', (sid, decisions, pending, context_next, summary))
|
|
142
|
+
conn.commit()
|
|
143
|
+
conn.close()
|
|
144
|
+
" 2>/dev/null || true
|
|
145
|
+
fi
|
|
146
|
+
|
|
45
147
|
cat << HOOKEOF
|
|
46
148
|
{
|
|
47
|
-
"systemMessage": "CONTEXT IS ABOUT TO BE COMPRESSED.\n\nOBLIGATORY ACTIONS BEFORE COMPACTION:\n1. Save critical state via MCP: nexo_checkpoint_save with current task, active files, decisions, errors, next step, and reasoning thread.\n2. If there is work in progress without a commit, save data via nexo_entity_create, nexo_preference_set, nexo_learning_add, nexo_followup_create.\n3. PERSISTENT TOOL LOGS: ${NEXO_HOME}/operations/tool-logs/${TODAY}.jsonl has ${LOG_LINES} entries.\n4. After compaction, the PostCompact hook will re-inject a Core Memory Block with the checkpoint.\n5. MCP tools (nexo_*) preserve all state — use them to recover context."
|
|
149
|
+
"systemMessage": "CONTEXT IS ABOUT TO BE COMPRESSED.\n\nOBLIGATORY ACTIONS BEFORE COMPACTION:\n1. Save critical state via MCP: nexo_checkpoint_save with current task, active files, decisions, errors, next step, and reasoning thread.\n2. If there is work in progress without a commit, save data via nexo_entity_create, nexo_preference_set, nexo_learning_add, nexo_followup_create.\n3. PERSISTENT TOOL LOGS: ${NEXO_HOME}/operations/tool-logs/${TODAY}.jsonl has ${LOG_LINES} entries.\n4. After compaction, the PostCompact hook will re-inject a Core Memory Block with the checkpoint.\n5. MCP tools (nexo_*) preserve all state — use them to recover context.\n6. EMERGENCY DIARY: An automatic diary was written by the pre-compact hook. The LLM can still write a better one via nexo_session_diary_write."
|
|
48
150
|
}
|
|
49
151
|
HOOKEOF
|
|
@@ -48,7 +48,7 @@ from datetime import date
|
|
|
48
48
|
today_str = '$TODAY'
|
|
49
49
|
weekday = '$WEEKDAY'
|
|
50
50
|
nexo_home = os.environ.get('NEXO_HOME', os.path.expanduser('~/.nexo'))
|
|
51
|
-
db_path = os.path.join(nexo_home, 'nexo.db')
|
|
51
|
+
db_path = os.path.join(nexo_home, 'data', 'nexo.db')
|
|
52
52
|
|
|
53
53
|
lines = []
|
|
54
54
|
lines.append(f'## Date: {today_str} ({weekday})')
|
|
@@ -212,7 +212,11 @@ except Exception as e:
|
|
|
212
212
|
fi
|
|
213
213
|
|
|
214
214
|
# ─── Cortex Report: what happened while user was away ───
|
|
215
|
-
|
|
215
|
+
# Check brain/ (canonical) first, fall back to cortex/ (legacy)
|
|
216
|
+
CORTEX_BRIEFING="$NEXO_HOME/brain/last-briefing.json"
|
|
217
|
+
if [ ! -f "$CORTEX_BRIEFING" ] && [ -f "$NEXO_HOME/cortex/last-briefing.json" ]; then
|
|
218
|
+
CORTEX_BRIEFING="$NEXO_HOME/cortex/last-briefing.json"
|
|
219
|
+
fi
|
|
216
220
|
if [ -f "$CORTEX_BRIEFING" ]; then
|
|
217
221
|
CORTEX_SECTION=$(python3 -c "
|
|
218
222
|
import json
|
|
@@ -25,7 +25,8 @@ TOOL_LOG="$NEXO_HOME/operations/tool-logs/${TODAY}.jsonl"
|
|
|
25
25
|
python3 -c "
|
|
26
26
|
import sys, json, os
|
|
27
27
|
nexo_home = os.environ.get('NEXO_HOME', os.path.expanduser('~/.nexo'))
|
|
28
|
-
|
|
28
|
+
nexo_code = os.environ.get('NEXO_CODE', nexo_home)
|
|
29
|
+
sys.path.insert(0, nexo_code)
|
|
29
30
|
os.environ['NEXO_SKIP_FS_INDEX'] = '1'
|
|
30
31
|
from db import init_db, get_db, get_active_sessions, upsert_diary_draft, get_diary_draft
|
|
31
32
|
init_db()
|
package/src/kg_populate.py
CHANGED
|
@@ -13,7 +13,10 @@ from db import get_db
|
|
|
13
13
|
|
|
14
14
|
def _cognitive_db():
|
|
15
15
|
"""Direct cognitive.db connection (for somatic_markers)."""
|
|
16
|
-
|
|
16
|
+
nexo_home = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
|
|
17
|
+
data_dir = os.path.join(nexo_home, "data")
|
|
18
|
+
os.makedirs(data_dir, exist_ok=True)
|
|
19
|
+
path = os.path.join(data_dir, "cognitive.db")
|
|
17
20
|
conn = sqlite3.connect(path)
|
|
18
21
|
conn.row_factory = sqlite3.Row
|
|
19
22
|
return conn
|
|
@@ -15,7 +15,10 @@ import sys
|
|
|
15
15
|
import time
|
|
16
16
|
import numpy as np
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
|
|
19
|
+
_data_dir = os.path.join(NEXO_HOME, "data")
|
|
20
|
+
os.makedirs(_data_dir, exist_ok=True)
|
|
21
|
+
DB_PATH = os.path.join(_data_dir, "cognitive.db")
|
|
19
22
|
BACKUP_PATH = DB_PATH + ".bak-384dims-pre-upgrade"
|
|
20
23
|
|
|
21
24
|
MODELS = {
|