nexo-brain 2.2.0 → 2.3.1
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 +5 -5
- package/package.json +6 -3
- package/src/auto_update.py +26 -0
- package/src/crons/manifest.json +6 -13
- package/src/crons/sync.py +150 -6
- package/src/db/__init__.py +13 -0
- package/src/db/_core.py +1 -0
- package/src/db/_cron_runs.py +74 -0
- package/src/db/_entities.py +1 -0
- package/src/db/_episodic.py +41 -6
- package/src/db/_learnings.py +1 -0
- package/src/db/_reminders.py +1 -0
- package/src/db/_schema.py +64 -0
- package/src/db/_sessions.py +1 -0
- package/src/db/_skills.py +515 -0
- package/src/hooks/session-stop.sh +13 -101
- package/src/plugin_loader.py +1 -0
- package/src/plugins/episodic_memory.py +5 -3
- package/src/plugins/schedule.py +212 -0
- package/src/plugins/skills.py +264 -0
- package/src/plugins/update.py +1 -0
- package/src/scripts/deep-sleep/apply_findings.py +111 -8
- package/src/scripts/deep-sleep/collect.py +34 -11
- package/src/scripts/deep-sleep/extract-prompt.md +38 -0
- package/src/scripts/deep-sleep/extract.py +81 -8
- package/src/scripts/deep-sleep/synthesize-prompt.md +29 -1
- package/src/scripts/deep-sleep/synthesize.py +4 -1
- package/src/scripts/nexo-catchup.py +65 -29
- package/src/scripts/nexo-cron-wrapper.sh +53 -0
- package/src/scripts/nexo-daily-self-audit.py +4 -2
- package/src/scripts/nexo-deep-sleep.sh +66 -77
- package/src/scripts/nexo-evolution-run.py +13 -0
- package/src/scripts/nexo-learning-housekeep.py +157 -1
- package/src/scripts/nexo-learning-validator.py +19 -0
- package/src/scripts/nexo-postmortem-consolidator.py +3 -2
- package/src/scripts/nexo-sleep.py +16 -11
- package/src/scripts/nexo-synthesis.py +46 -3
- package/src/scripts/nexo-watchdog.sh +91 -30
- package/src/server.py +6 -1
- package/src/tools_coordination.py +1 -0
- package/src/tools_sessions.py +1 -0
- package/scripts/migrate-to-unified 2.sh +0 -813
- package/scripts/migrate-to-unified.sh +0 -813
- package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
- package/scripts/migrate-v1.5-to-v1.6.py +0 -778
- package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
- package/scripts/migrate-v1.7-to-v1.8.py +0 -214
- package/scripts/pre-commit-check 2.sh +0 -55
- package/scripts/pre-commit-check.sh +0 -55
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/auto_close_sessions 2.py +0 -159
- package/src/auto_update 2.py +0 -634
- package/src/claim_graph 2.py +0 -323
- package/src/cognitive/__init__ 2.py +0 -62
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/_core 2.py +0 -567
- package/src/cognitive/_decay 2.py +0 -382
- package/src/cognitive/_ingest 2.py +0 -892
- package/src/cognitive/_memory 2.py +0 -912
- package/src/cognitive/_search 2.py +0 -949
- package/src/cognitive/_trust 2.py +0 -464
- package/src/crons/manifest 2.json +0 -106
- package/src/crons/sync 2.py +0 -217
- package/src/dashboard/__init__ 2.py +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app 2.py +0 -789
- package/src/db/__init__ 2.py +0 -89
- 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 2.py +0 -417
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_entities 2.py +0 -178
- package/src/db/_episodic 2.py +0 -738
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_learnings 2.py +0 -168
- package/src/db/_reminders 2.py +0 -338
- package/src/db/_schema 2.py +0 -364
- package/src/db/_sessions 2.py +0 -300
- package/src/db/_tasks 2.py +0 -91
- package/src/evolution_cycle 2.py +0 -266
- package/src/hnsw_index 2.py +0 -254
- package/src/hooks/auto_capture 2.py +0 -208
- package/src/hooks/caffeinate-guard 2.sh +0 -8
- package/src/hooks/capture-session 2.sh +0 -21
- package/src/hooks/capture-tool-logs 2.sh +0 -127
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -148
- package/src/hooks/pre-compact 2.sh +0 -151
- package/src/hooks/session-start 2.sh +0 -268
- package/src/hooks/session-stop 2.sh +0 -140
- package/src/kg_populate 2.py +0 -290
- package/src/knowledge_graph 2.py +0 -257
- package/src/maintenance 2.py +0 -59
- package/src/migrate_embeddings 2.py +0 -122
- package/src/plugin_loader 2.py +0 -202
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/adaptive_mode 2.py +0 -805
- package/src/plugins/agents 2.py +0 -52
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -104
- package/src/plugins/cognitive_memory 2.py +0 -564
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -299
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -533
- package/src/plugins/evolution 2.py +0 -115
- package/src/plugins/guard 2.py +0 -746
- package/src/plugins/knowledge_graph_tools 2.py +0 -105
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/update 2.py +0 -256
- package/src/requirements 2.txt +0 -12
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +0 -331
- package/src/rules/migrate 2.py +0 -207
- package/src/scripts/check-context 2.py +0 -264
- package/src/scripts/nexo-auto-update 2.py +0 -6
- package/src/scripts/nexo-backup 2.sh +0 -25
- package/src/scripts/nexo-brain-activation 2.sh +0 -140
- package/src/scripts/nexo-catchup 2.py +0 -242
- package/src/scripts/nexo-cognitive-decay 2.py +0 -182
- package/src/scripts/nexo-daily-self-audit 2.py +0 -552
- package/src/scripts/nexo-deep-sleep 2.sh +0 -97
- package/src/scripts/nexo-evolution-run 2.py +0 -597
- package/src/scripts/nexo-followup-hygiene 2.py +0 -112
- package/src/scripts/nexo-github-monitor 2.py +0 -256
- package/src/scripts/nexo-github-monitor.py +0 -256
- package/src/scripts/nexo-immune 2.py +0 -927
- package/src/scripts/nexo-inbox-hook 2.sh +0 -74
- package/src/scripts/nexo-install 2.py +0 -6
- package/src/scripts/nexo-learning-housekeep 2.py +0 -245
- package/src/scripts/nexo-learning-validator 2.py +0 -207
- package/src/scripts/nexo-migrate 2.py +0 -232
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
- package/src/scripts/nexo-reflection 2.py +0 -253
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-send-email 2.py +0 -25
- package/src/scripts/nexo-send-email.py +0 -25
- package/src/scripts/nexo-send-reply 2.py +0 -178
- package/src/scripts/nexo-send-reply.py +0 -178
- package/src/scripts/nexo-sleep 2.py +0 -592
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-synthesis 2.py +0 -253
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -161
- package/src/scripts/nexo-watchdog 2.sh +0 -878
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/server 2.py +0 -733
- package/src/storage_router 2.py +0 -32
- package/src/tools_coordination 2.py +0 -102
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_learnings 2.py +0 -220
- package/src/tools_menu 2.py +0 -227
- package/src/tools_reminders 2.py +0 -86
- package/src/tools_reminders_crud 2.py +0 -159
- package/src/tools_sessions 2.py +0 -476
- package/src/tools_task_history 2.py +0 -57
- package/templates/CLAUDE.md 2.template +0 -63
- package/templates/openclaw 2.json +0 -13
- package/tests/__init__ 2.py +0 -0
- package/tests/__init__.py +0 -0
- package/tests/conftest 2.py +0 -71
- package/tests/conftest.py +0 -71
- package/tests/test_cognitive 2.py +0 -205
- package/tests/test_cognitive.py +0 -205
- package/tests/test_knowledge_graph 2.py +0 -140
- package/tests/test_knowledge_graph.py +0 -140
- package/tests/test_migrations 2.py +0 -137
- package/tests/test_migrations.py +0 -137
package/README.md
CHANGED
|
@@ -283,13 +283,13 @@ NEXO Brain doesn't just respond — it runs 15 autonomous processes in the backg
|
|
|
283
283
|
| **prevent-sleep** | Always (daemon) | Keeps machine awake for nocturnal processes (caffeinate/systemd-inhibit) |
|
|
284
284
|
| **evolution** | Weekly (Sun) | Self-improvement proposals — NEXO suggests and applies enhancements |
|
|
285
285
|
| **followup-hygiene** | Weekly (Sun) | Normalizes statuses, flags stale followups, cleans orphans |
|
|
286
|
+
| **learning-housekeep** | 03:15 daily | Dedup learnings, adjust weights by usage, process overdue reviews, reconcile decision outcomes |
|
|
286
287
|
| **immune** | Every 30 min | Quarantine processing, memory promotion/rejection, synaptic pruning |
|
|
287
|
-
| **synthesis** |
|
|
288
|
-
| **
|
|
289
|
-
| **watchdog** | Every 5 min | Monitors services, LaunchAgents, and infrastructure health |
|
|
288
|
+
| **synthesis** | 06:00 daily | Memory synthesis — discovers cross-memory patterns |
|
|
289
|
+
| **watchdog** | Every 30 min | Monitors services, LaunchAgents, and infrastructure health |
|
|
290
290
|
| **auto-close-sessions** | Every 5 min | Cleans stale sessions |
|
|
291
291
|
|
|
292
|
-
|
|
292
|
+
Core processes are defined in `src/crons/manifest.json` and auto-synced to your system by `nexo_update`. On macOS they run via LaunchAgents; on Linux via systemd user timers. `tcc-approve`, `prevent-sleep`, and `backup` are platform/personal helpers — not in the manifest but listed above for completeness. Personal crons (your own scripts) are never touched by the sync. If your Mac was asleep during a scheduled process, the catch-up script re-runs everything in order when it wakes.
|
|
293
293
|
|
|
294
294
|
## Deep Sleep v2 — Overnight Learning (v2.1.0)
|
|
295
295
|
|
|
@@ -755,7 +755,7 @@ If NEXO Brain is useful to you, consider:
|
|
|
755
755
|
- **Auto-resolve followups**: Change log entries automatically cross-reference and complete matching open followups.
|
|
756
756
|
- **Free-form learning categories**: No more hardcoded category validation — use any category name.
|
|
757
757
|
- **CLAUDE.md template rewrite**: 494 to 127 lines, compact procedural format with full heartbeat signal reactions.
|
|
758
|
-
- **Complete sanitization**: All hardcoded paths use `NEXO_HOME` env var.
|
|
758
|
+
- **Complete sanitization**: All hardcoded paths use `NEXO_HOME` env var. No credentials or personal data in the distributed package. Migration scripts and maintainer tooling use configurable paths.
|
|
759
759
|
|
|
760
760
|
### v1.6.0 — Nervous System + Dashboard v2 (2026-03-30)
|
|
761
761
|
- **Nervous System**: 11 autonomous scripts (decay, deep sleep, self-audit, catchup, evolution, followup hygiene, immune, watchdog, github monitor, learning validator)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO — Cognitive co-operator for Claude Code. Memory, emotional intelligence, overnight learning (Deep Sleep), cron management, trust scoring, and adaptive calibration.",
|
|
6
6
|
"bin": {
|
|
@@ -56,8 +56,11 @@
|
|
|
56
56
|
"bin/nexo-brain.js",
|
|
57
57
|
"bin/postinstall.js",
|
|
58
58
|
"src/",
|
|
59
|
+
"!src/**/__pycache__",
|
|
60
|
+
"!src/**/*.pyc",
|
|
61
|
+
"!src/**/*.pyo",
|
|
59
62
|
"templates/",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
63
|
+
"README.md",
|
|
64
|
+
"LICENSE"
|
|
62
65
|
]
|
|
63
66
|
}
|
package/src/auto_update.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
"""NEXO Auto-Update — lightweight startup check for git updates and file-based migrations.
|
|
2
3
|
|
|
3
4
|
Called once per server startup. Respects a 1-hour cooldown to avoid redundant checks.
|
|
@@ -93,6 +94,27 @@ def _read_package_version() -> str:
|
|
|
93
94
|
return "unknown"
|
|
94
95
|
|
|
95
96
|
|
|
97
|
+
# ── Hook sync ────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
def _sync_hooks():
|
|
100
|
+
"""Copy hook scripts from src/hooks/ to NEXO_HOME/hooks/ after a git pull."""
|
|
101
|
+
import shutil
|
|
102
|
+
hooks_src = SRC_DIR / "hooks"
|
|
103
|
+
hooks_dest = NEXO_HOME / "hooks"
|
|
104
|
+
if not hooks_src.is_dir():
|
|
105
|
+
return
|
|
106
|
+
hooks_dest.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
synced = 0
|
|
108
|
+
for f in hooks_src.iterdir():
|
|
109
|
+
if f.is_file() and f.suffix == ".sh":
|
|
110
|
+
dest = hooks_dest / f.name
|
|
111
|
+
shutil.copy2(str(f), str(dest))
|
|
112
|
+
os.chmod(str(dest), 0o755)
|
|
113
|
+
synced += 1
|
|
114
|
+
if synced:
|
|
115
|
+
_log(f"Synced {synced} hook(s) to {hooks_dest}")
|
|
116
|
+
|
|
117
|
+
|
|
96
118
|
# ── Git-based auto-update ────────────────────────────────────────────
|
|
97
119
|
|
|
98
120
|
def _check_git_updates() -> str | None:
|
|
@@ -140,6 +162,10 @@ def _check_git_updates() -> str | None:
|
|
|
140
162
|
# Run DB migrations after pull
|
|
141
163
|
_run_db_migrations()
|
|
142
164
|
|
|
165
|
+
# Sync hooks to NEXO_HOME (nexo-brain.js copies them on install,
|
|
166
|
+
# but auto-update via git pull bypasses nexo-brain.js)
|
|
167
|
+
_sync_hooks()
|
|
168
|
+
|
|
143
169
|
msg = f"Auto-updated: {old_version} -> {new_version}" if old_version != new_version else f"Auto-updated (v{new_version}, new commits)"
|
|
144
170
|
_log(msg)
|
|
145
171
|
return msg
|
package/src/crons/manifest.json
CHANGED
|
@@ -70,35 +70,28 @@
|
|
|
70
70
|
{
|
|
71
71
|
"id": "followup-hygiene",
|
|
72
72
|
"script": "scripts/nexo-followup-hygiene.py",
|
|
73
|
-
"schedule": {"hour": 5, "minute": 0},
|
|
73
|
+
"schedule": {"hour": 5, "minute": 0, "weekday": 0},
|
|
74
74
|
"description": "Clean stale followups, archive completed, validate dates",
|
|
75
75
|
"core": true
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
"id": "synthesis",
|
|
79
79
|
"script": "scripts/nexo-synthesis.py",
|
|
80
|
-
"
|
|
81
|
-
"description": "
|
|
80
|
+
"schedule": {"hour": 6, "minute": 0},
|
|
81
|
+
"description": "Daily synthesis — cross-reference learnings, decisions, changes",
|
|
82
82
|
"core": true
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
"id": "auto-close-sessions",
|
|
86
|
-
"script": "
|
|
86
|
+
"script": "auto_close_sessions.py",
|
|
87
87
|
"interval_seconds": 300,
|
|
88
88
|
"description": "Close stale sessions that lost their parent process",
|
|
89
89
|
"core": true
|
|
90
90
|
},
|
|
91
|
-
|
|
92
|
-
"id": "github-monitor",
|
|
93
|
-
"script": "scripts/nexo-github-monitor.py",
|
|
94
|
-
"schedule": {"hour": 8, "minute": 0},
|
|
95
|
-
"description": "Monitor GitHub repo — issues, PRs, stars, auto-respond",
|
|
96
|
-
"core": true
|
|
97
|
-
},
|
|
98
|
-
{
|
|
91
|
+
{
|
|
99
92
|
"id": "catchup",
|
|
100
93
|
"script": "scripts/nexo-catchup.py",
|
|
101
|
-
"
|
|
94
|
+
"run_at_load": true,
|
|
102
95
|
"description": "Morning catchup briefing for the user",
|
|
103
96
|
"core": true
|
|
104
97
|
}
|
package/src/crons/sync.py
CHANGED
|
@@ -42,15 +42,55 @@ def load_manifest() -> list[dict]:
|
|
|
42
42
|
return data.get("crons", [])
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def _copy_script_to_nexo_home(src: Path) -> Path:
|
|
46
|
+
"""Copy a script from NEXO_CODE to NEXO_HOME/scripts/ for Sandbox compatibility.
|
|
47
|
+
|
|
48
|
+
macOS Sandbox blocks LaunchAgents from executing scripts in ~/Documents/.
|
|
49
|
+
We copy scripts to NEXO_HOME/scripts/ which is outside the Sandbox restricted paths.
|
|
50
|
+
"""
|
|
51
|
+
dest_dir = NEXO_HOME / "scripts"
|
|
52
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
if src.is_dir():
|
|
55
|
+
import shutil
|
|
56
|
+
dest = dest_dir / src.name
|
|
57
|
+
if dest.exists():
|
|
58
|
+
shutil.rmtree(dest)
|
|
59
|
+
shutil.copytree(src, dest)
|
|
60
|
+
return dest
|
|
61
|
+
else:
|
|
62
|
+
dest = dest_dir / src.name
|
|
63
|
+
import shutil
|
|
64
|
+
shutil.copy2(src, dest)
|
|
65
|
+
dest.chmod(0o755)
|
|
66
|
+
return dest
|
|
67
|
+
|
|
68
|
+
|
|
45
69
|
def build_plist(cron: dict) -> dict:
|
|
46
70
|
"""Build a macOS LaunchAgent plist dict from a manifest entry."""
|
|
47
71
|
cron_id = cron["id"]
|
|
48
72
|
label = f"{LABEL_PREFIX}{cron_id}"
|
|
49
|
-
|
|
73
|
+
script_src = NEXO_CODE / cron["script"]
|
|
50
74
|
script_type = cron.get("type", "python")
|
|
51
75
|
|
|
76
|
+
# Copy scripts to NEXO_HOME/scripts/ to avoid macOS Sandbox restrictions
|
|
77
|
+
script_dest = _copy_script_to_nexo_home(script_src)
|
|
78
|
+
script_path = str(script_dest)
|
|
79
|
+
|
|
80
|
+
# Also copy the wrapper and any subdirectories (e.g., deep-sleep/)
|
|
81
|
+
wrapper_src = NEXO_CODE / "scripts" / "nexo-cron-wrapper.sh"
|
|
82
|
+
wrapper_dest = _copy_script_to_nexo_home(wrapper_src)
|
|
83
|
+
wrapper_path = str(wrapper_dest)
|
|
84
|
+
|
|
85
|
+
# Copy script subdirectories if they exist (e.g., deep-sleep/ for nexo-deep-sleep.sh)
|
|
86
|
+
script_name = script_src.stem # e.g., "nexo-deep-sleep"
|
|
87
|
+
subdir_name = script_name.replace("nexo-", "") # e.g., "deep-sleep"
|
|
88
|
+
subdir_src = NEXO_CODE / "scripts" / subdir_name
|
|
89
|
+
if subdir_src.is_dir():
|
|
90
|
+
_copy_script_to_nexo_home(subdir_src)
|
|
91
|
+
|
|
52
92
|
if script_type == "shell":
|
|
53
|
-
program_args = ["/bin/bash", script_path]
|
|
93
|
+
program_args = ["/bin/bash", wrapper_path, cron_id, "/bin/bash", script_path]
|
|
54
94
|
else:
|
|
55
95
|
# Find python3
|
|
56
96
|
python_candidates = [
|
|
@@ -64,7 +104,7 @@ def build_plist(cron: dict) -> dict:
|
|
|
64
104
|
if Path(p).exists():
|
|
65
105
|
python_bin = p
|
|
66
106
|
break
|
|
67
|
-
program_args = [python_bin, script_path]
|
|
107
|
+
program_args = ["/bin/bash", wrapper_path, cron_id, python_bin, script_path]
|
|
68
108
|
|
|
69
109
|
plist = {
|
|
70
110
|
"Label": label,
|
|
@@ -84,7 +124,9 @@ def build_plist(cron: dict) -> dict:
|
|
|
84
124
|
}
|
|
85
125
|
|
|
86
126
|
# Schedule
|
|
87
|
-
if "
|
|
127
|
+
if cron.get("run_at_load"):
|
|
128
|
+
plist["RunAtLoad"] = True
|
|
129
|
+
elif "interval_seconds" in cron:
|
|
88
130
|
plist["StartInterval"] = cron["interval_seconds"]
|
|
89
131
|
elif "schedule" in cron:
|
|
90
132
|
cal = {}
|
|
@@ -126,6 +168,8 @@ def plist_needs_update(existing_path: Path, new_plist: dict) -> bool:
|
|
|
126
168
|
return True
|
|
127
169
|
if existing.get("StartCalendarInterval") != new_plist.get("StartCalendarInterval"):
|
|
128
170
|
return True
|
|
171
|
+
if existing.get("RunAtLoad") != new_plist.get("RunAtLoad"):
|
|
172
|
+
return True
|
|
129
173
|
return False
|
|
130
174
|
|
|
131
175
|
|
|
@@ -157,8 +201,12 @@ def unload_plist(plist_path: Path, dry_run: bool):
|
|
|
157
201
|
|
|
158
202
|
|
|
159
203
|
def sync(dry_run: bool = False):
|
|
160
|
-
|
|
161
|
-
|
|
204
|
+
system = platform.system()
|
|
205
|
+
if system == "Linux":
|
|
206
|
+
sync_linux(dry_run)
|
|
207
|
+
return
|
|
208
|
+
if system != "Darwin":
|
|
209
|
+
log(f"Unsupported platform: {system}. Skipping.")
|
|
162
210
|
return
|
|
163
211
|
|
|
164
212
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -210,6 +258,102 @@ def sync(dry_run: bool = False):
|
|
|
210
258
|
log("Sync complete.")
|
|
211
259
|
|
|
212
260
|
|
|
261
|
+
def sync_linux(dry_run: bool = False):
|
|
262
|
+
"""Sync manifest to systemd user timers (Linux)."""
|
|
263
|
+
unit_dir = Path.home() / ".config" / "systemd" / "user"
|
|
264
|
+
unit_dir.mkdir(parents=True, exist_ok=True)
|
|
265
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
266
|
+
|
|
267
|
+
manifest_crons = load_manifest()
|
|
268
|
+
wrapper_src = NEXO_CODE / "scripts" / "nexo-cron-wrapper.sh"
|
|
269
|
+
wrapper_dest = _copy_script_to_nexo_home(wrapper_src)
|
|
270
|
+
|
|
271
|
+
log(f"Manifest: {len(manifest_crons)} core crons")
|
|
272
|
+
|
|
273
|
+
python_bin = "/usr/bin/python3"
|
|
274
|
+
for p in ["/usr/bin/python3", "/usr/local/bin/python3"]:
|
|
275
|
+
if Path(p).exists():
|
|
276
|
+
python_bin = p
|
|
277
|
+
break
|
|
278
|
+
|
|
279
|
+
for cron in manifest_crons:
|
|
280
|
+
cron_id = cron["id"]
|
|
281
|
+
script_src = NEXO_CODE / cron["script"]
|
|
282
|
+
script_dest = _copy_script_to_nexo_home(script_src)
|
|
283
|
+
script_type = cron.get("type", "python")
|
|
284
|
+
|
|
285
|
+
# Copy subdirectories
|
|
286
|
+
subdir_name = script_src.stem.replace("nexo-", "")
|
|
287
|
+
subdir_src = NEXO_CODE / "scripts" / subdir_name
|
|
288
|
+
if subdir_src.is_dir():
|
|
289
|
+
_copy_script_to_nexo_home(subdir_src)
|
|
290
|
+
|
|
291
|
+
if script_type == "shell":
|
|
292
|
+
exec_cmd = f"/bin/bash {wrapper_dest} {cron_id} /bin/bash {script_dest}"
|
|
293
|
+
else:
|
|
294
|
+
exec_cmd = f"/bin/bash {wrapper_dest} {cron_id} {python_bin} {script_dest}"
|
|
295
|
+
|
|
296
|
+
service_path = unit_dir / f"nexo-{cron_id}.service"
|
|
297
|
+
timer_path = unit_dir / f"nexo-{cron_id}.timer"
|
|
298
|
+
|
|
299
|
+
service_content = f"""[Unit]
|
|
300
|
+
Description=NEXO: {cron.get('description', cron_id)}
|
|
301
|
+
|
|
302
|
+
[Service]
|
|
303
|
+
Type=oneshot
|
|
304
|
+
ExecStart={exec_cmd}
|
|
305
|
+
Environment=NEXO_HOME={NEXO_HOME}
|
|
306
|
+
Environment=NEXO_CODE={NEXO_CODE}
|
|
307
|
+
Environment=HOME={Path.home()}
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
if cron.get("run_at_load"):
|
|
311
|
+
timer_spec = "OnBootSec=0"
|
|
312
|
+
elif "interval_seconds" in cron:
|
|
313
|
+
timer_spec = f"OnUnitActiveSec={cron['interval_seconds']}s\nOnBootSec=60s"
|
|
314
|
+
elif "schedule" in cron:
|
|
315
|
+
s = cron["schedule"]
|
|
316
|
+
h, m = s.get("hour", 0), s.get("minute", 0)
|
|
317
|
+
if "weekday" in s:
|
|
318
|
+
days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
|
319
|
+
timer_spec = f"OnCalendar={days[s['weekday']]} *-*-* {h:02d}:{m:02d}:00"
|
|
320
|
+
else:
|
|
321
|
+
timer_spec = f"OnCalendar=*-*-* {h:02d}:{m:02d}:00"
|
|
322
|
+
else:
|
|
323
|
+
log(f" SKIP {cron_id}: no schedule or interval")
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
timer_content = f"""[Unit]
|
|
327
|
+
Description=NEXO timer: {cron.get('description', cron_id)}
|
|
328
|
+
|
|
329
|
+
[Timer]
|
|
330
|
+
{timer_spec}
|
|
331
|
+
Persistent=true
|
|
332
|
+
|
|
333
|
+
[Install]
|
|
334
|
+
WantedBy=timers.target
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
if dry_run:
|
|
338
|
+
log(f" DRY-RUN: would install {cron_id}")
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
service_path.write_text(service_content)
|
|
342
|
+
timer_path.write_text(timer_content)
|
|
343
|
+
log(f" Installed: {cron_id}")
|
|
344
|
+
|
|
345
|
+
if not dry_run:
|
|
346
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
|
|
347
|
+
for cron in manifest_crons:
|
|
348
|
+
subprocess.run(
|
|
349
|
+
["systemctl", "--user", "enable", "--now", f"nexo-{cron['id']}.timer"],
|
|
350
|
+
capture_output=True
|
|
351
|
+
)
|
|
352
|
+
log("systemd timers enabled.")
|
|
353
|
+
|
|
354
|
+
log("Sync complete.")
|
|
355
|
+
|
|
356
|
+
|
|
213
357
|
if __name__ == "__main__":
|
|
214
358
|
dry_run = "--dry-run" in sys.argv
|
|
215
359
|
if dry_run:
|
package/src/db/__init__.py
CHANGED
|
@@ -87,3 +87,16 @@ from db._evolution import (
|
|
|
87
87
|
insert_evolution_metric, get_latest_metrics,
|
|
88
88
|
insert_evolution_log, get_evolution_history, update_evolution_log_status,
|
|
89
89
|
)
|
|
90
|
+
|
|
91
|
+
# Cron execution history
|
|
92
|
+
from db._cron_runs import (
|
|
93
|
+
cron_run_start, cron_run_end, cron_runs_recent, cron_runs_summary,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Skills
|
|
97
|
+
from db._skills import (
|
|
98
|
+
create_skill, get_skill, list_skills, search_skills,
|
|
99
|
+
update_skill, delete_skill,
|
|
100
|
+
record_usage as record_skill_usage,
|
|
101
|
+
match_skills, merge_skills, get_skill_stats, decay_unused_skills,
|
|
102
|
+
)
|
package/src/db/_core.py
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""NEXO DB — Cron execution history."""
|
|
2
|
+
from db._core import get_db
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def cron_run_start(cron_id: str) -> int:
|
|
6
|
+
"""Record a cron starting. Returns the run ID."""
|
|
7
|
+
conn = get_db()
|
|
8
|
+
cursor = conn.execute(
|
|
9
|
+
"INSERT INTO cron_runs (cron_id) VALUES (?)", (cron_id,)
|
|
10
|
+
)
|
|
11
|
+
conn.commit()
|
|
12
|
+
return cursor.lastrowid
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def cron_run_end(run_id: int, exit_code: int, summary: str = '', error: str = ''):
|
|
16
|
+
"""Record a cron finishing."""
|
|
17
|
+
conn = get_db()
|
|
18
|
+
conn.execute(
|
|
19
|
+
"""UPDATE cron_runs
|
|
20
|
+
SET ended_at = datetime('now'),
|
|
21
|
+
exit_code = ?,
|
|
22
|
+
summary = ?,
|
|
23
|
+
error = ?,
|
|
24
|
+
duration_secs = ROUND((julianday(datetime('now')) - julianday(started_at)) * 86400, 1)
|
|
25
|
+
WHERE id = ?""",
|
|
26
|
+
(exit_code, summary[:500], error[:500], run_id)
|
|
27
|
+
)
|
|
28
|
+
conn.commit()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cron_runs_recent(hours: int = 24, cron_id: str = '') -> list[dict]:
|
|
32
|
+
"""Get recent cron executions."""
|
|
33
|
+
conn = get_db()
|
|
34
|
+
if cron_id:
|
|
35
|
+
rows = conn.execute(
|
|
36
|
+
"""SELECT * FROM cron_runs
|
|
37
|
+
WHERE cron_id = ? AND started_at >= datetime('now', ?)
|
|
38
|
+
ORDER BY started_at DESC""",
|
|
39
|
+
(cron_id, f"-{hours} hours")
|
|
40
|
+
).fetchall()
|
|
41
|
+
else:
|
|
42
|
+
rows = conn.execute(
|
|
43
|
+
"""SELECT * FROM cron_runs
|
|
44
|
+
WHERE started_at >= datetime('now', ?)
|
|
45
|
+
ORDER BY started_at DESC""",
|
|
46
|
+
(f"-{hours} hours",)
|
|
47
|
+
).fetchall()
|
|
48
|
+
return [dict(r) for r in rows]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def cron_runs_summary(hours: int = 24) -> list[dict]:
|
|
52
|
+
"""Get summary per cron: last run, success rate, avg duration."""
|
|
53
|
+
conn = get_db()
|
|
54
|
+
rows = conn.execute(
|
|
55
|
+
"""SELECT
|
|
56
|
+
cron_id,
|
|
57
|
+
COUNT(*) as total_runs,
|
|
58
|
+
SUM(CASE WHEN exit_code = 0 THEN 1 ELSE 0 END) as succeeded,
|
|
59
|
+
SUM(CASE WHEN exit_code != 0 OR exit_code IS NULL THEN 1 ELSE 0 END) as failed,
|
|
60
|
+
ROUND(AVG(duration_secs), 1) as avg_duration,
|
|
61
|
+
MAX(started_at) as last_run,
|
|
62
|
+
(SELECT exit_code FROM cron_runs cr2
|
|
63
|
+
WHERE cr2.cron_id = cron_runs.cron_id
|
|
64
|
+
ORDER BY started_at DESC LIMIT 1) as last_exit_code,
|
|
65
|
+
(SELECT summary FROM cron_runs cr3
|
|
66
|
+
WHERE cr3.cron_id = cron_runs.cron_id AND cr3.summary != ''
|
|
67
|
+
ORDER BY started_at DESC LIMIT 1) as last_summary
|
|
68
|
+
FROM cron_runs
|
|
69
|
+
WHERE started_at >= datetime('now', ?)
|
|
70
|
+
GROUP BY cron_id
|
|
71
|
+
ORDER BY last_run DESC""",
|
|
72
|
+
(f"-{hours} hours",)
|
|
73
|
+
).fetchall()
|
|
74
|
+
return [dict(r) for r in rows]
|
package/src/db/_entities.py
CHANGED
package/src/db/_episodic.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
"""NEXO DB — Episodic module."""
|
|
2
3
|
import datetime, time, json
|
|
3
4
|
from db._core import get_db, now_epoch, _multi_word_like
|
|
@@ -568,17 +569,35 @@ def get_orphan_sessions(ttl_seconds: int = 900) -> list[dict]:
|
|
|
568
569
|
|
|
569
570
|
|
|
570
571
|
def read_session_diary(session_id: str = '', last_n: int = 3, last_day: bool = False,
|
|
571
|
-
domain: str = '') -> list[dict]:
|
|
572
|
+
domain: str = '', include_automated: bool = False) -> list[dict]:
|
|
572
573
|
"""Read session diary entries.
|
|
573
574
|
|
|
574
575
|
- session_id: returns entries for that specific session
|
|
575
576
|
- last_day: returns ALL entries from the most recent day (multi-terminal aware)
|
|
576
577
|
- last_n: returns last N entries (default)
|
|
577
578
|
- domain: filter by project context (nexo, other)
|
|
579
|
+
- include_automated: if False (default), excludes automated sessions (auto-close,
|
|
580
|
+
cron diaries, etc.). Only returns human-interactive sessions.
|
|
581
|
+
Email sessions (user sends email, NEXO responds) ARE included — they're real interactions.
|
|
578
582
|
"""
|
|
579
583
|
conn = get_db()
|
|
580
584
|
domain_clause = " AND domain = ?" if domain else ""
|
|
581
585
|
domain_params = (domain,) if domain else ()
|
|
586
|
+
# By default, filter out automated sessions so startup shows human sessions only.
|
|
587
|
+
# Keeps: interactive sessions + auto-closed sessions that had real user interaction.
|
|
588
|
+
# An auto-close is human if it has heartbeats > 0 (heartbeat only fires on user messages).
|
|
589
|
+
# Excludes: cron jobs, auto-closed crons (0 heartbeats or "Minimal diary").
|
|
590
|
+
if include_automated:
|
|
591
|
+
source_clause = ""
|
|
592
|
+
else:
|
|
593
|
+
source_clause = (
|
|
594
|
+
" AND ("
|
|
595
|
+
" (source = 'claude' AND summary NOT LIKE '[AUTO-%')"
|
|
596
|
+
" OR (source = 'auto-close'"
|
|
597
|
+
" AND mental_state NOT LIKE '%0 heartbeats%'"
|
|
598
|
+
" AND mental_state NOT LIKE '%Minimal diary%')"
|
|
599
|
+
")"
|
|
600
|
+
)
|
|
582
601
|
|
|
583
602
|
if session_id:
|
|
584
603
|
rows = conn.execute(
|
|
@@ -586,25 +605,25 @@ def read_session_diary(session_id: str = '', last_n: int = 3, last_day: bool = F
|
|
|
586
605
|
(session_id,) + domain_params
|
|
587
606
|
).fetchall()
|
|
588
607
|
elif last_day:
|
|
589
|
-
# Get all entries from the most recent calendar day
|
|
608
|
+
# Get all entries from the most recent calendar day (human sessions only)
|
|
590
609
|
if domain:
|
|
591
610
|
latest = conn.execute(
|
|
592
|
-
"SELECT date(created_at) as day FROM session_diary WHERE domain = ? ORDER BY created_at DESC LIMIT 1",
|
|
611
|
+
f"SELECT date(created_at) as day FROM session_diary WHERE domain = ?{source_clause} ORDER BY created_at DESC LIMIT 1",
|
|
593
612
|
(domain,)
|
|
594
613
|
).fetchone()
|
|
595
614
|
else:
|
|
596
615
|
latest = conn.execute(
|
|
597
|
-
"SELECT date(created_at) as day FROM session_diary ORDER BY created_at DESC LIMIT 1"
|
|
616
|
+
f"SELECT date(created_at) as day FROM session_diary WHERE 1=1{source_clause} ORDER BY created_at DESC LIMIT 1"
|
|
598
617
|
).fetchone()
|
|
599
618
|
if not latest:
|
|
600
619
|
return []
|
|
601
620
|
rows = conn.execute(
|
|
602
|
-
f"SELECT * FROM session_diary WHERE date(created_at) = ?{domain_clause} ORDER BY created_at DESC",
|
|
621
|
+
f"SELECT * FROM session_diary WHERE date(created_at) = ?{domain_clause}{source_clause} ORDER BY created_at DESC",
|
|
603
622
|
(latest['day'],) + domain_params
|
|
604
623
|
).fetchall()
|
|
605
624
|
else:
|
|
606
625
|
rows = conn.execute(
|
|
607
|
-
f"SELECT * FROM session_diary WHERE 1=1{domain_clause} ORDER BY created_at DESC LIMIT ?",
|
|
626
|
+
f"SELECT * FROM session_diary WHERE 1=1{domain_clause}{source_clause} ORDER BY created_at DESC LIMIT ?",
|
|
608
627
|
domain_params + (last_n,)
|
|
609
628
|
).fetchall()
|
|
610
629
|
return [dict(r) for r in rows]
|
|
@@ -732,6 +751,22 @@ def recall(query: str, days: int = 30) -> list[dict]:
|
|
|
732
751
|
""", [cutoff_str] + params).fetchall()
|
|
733
752
|
results.extend([dict(r) for r in rows])
|
|
734
753
|
|
|
754
|
+
# Skills
|
|
755
|
+
try:
|
|
756
|
+
frag, params = _multi_word_like(query, ["name", "description", "tags", "trigger_patterns"])
|
|
757
|
+
rows = conn.execute(f"""
|
|
758
|
+
SELECT id, created_at, 'skill' AS source,
|
|
759
|
+
name AS title,
|
|
760
|
+
(COALESCE(description,'') || ' | ' || COALESCE(tags,'') || ' | ' || COALESCE(trigger_patterns,'')) AS snippet,
|
|
761
|
+
level AS category, 0 AS rank
|
|
762
|
+
FROM skills
|
|
763
|
+
WHERE created_at >= ? AND ({frag})
|
|
764
|
+
ORDER BY trust_score DESC LIMIT 10
|
|
765
|
+
""", [cutoff_str] + params).fetchall()
|
|
766
|
+
results.extend([dict(r) for r in rows])
|
|
767
|
+
except Exception:
|
|
768
|
+
pass # Table may not exist yet during migration
|
|
769
|
+
|
|
735
770
|
results.sort(key=lambda r: r.get('created_at', ''), reverse=True)
|
|
736
771
|
return results[:20]
|
|
737
772
|
|
package/src/db/_learnings.py
CHANGED
package/src/db/_reminders.py
CHANGED
package/src/db/_schema.py
CHANGED
|
@@ -295,7 +295,69 @@ def _m15_core_rules_tables(conn):
|
|
|
295
295
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_core_rules_active ON core_rules(is_active)")
|
|
296
296
|
|
|
297
297
|
|
|
298
|
+
def _m16_skills_tables(conn):
|
|
299
|
+
"""Skill Auto-Creation system — reusable procedures extracted from complex tasks.
|
|
300
|
+
|
|
301
|
+
Skills are procedural knowledge (step-by-step how-tos) vs learnings which are
|
|
302
|
+
declarative (don't do X). Pipeline: trace → draft → published, fully autonomous.
|
|
303
|
+
Trust score with decay controls quality without human approval gates.
|
|
304
|
+
"""
|
|
305
|
+
conn.execute("""
|
|
306
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
307
|
+
id TEXT PRIMARY KEY,
|
|
308
|
+
name TEXT NOT NULL,
|
|
309
|
+
description TEXT DEFAULT '',
|
|
310
|
+
level TEXT NOT NULL DEFAULT 'trace',
|
|
311
|
+
trust_score INTEGER NOT NULL DEFAULT 50,
|
|
312
|
+
file_path TEXT DEFAULT '',
|
|
313
|
+
tags TEXT DEFAULT '[]',
|
|
314
|
+
trigger_patterns TEXT DEFAULT '[]',
|
|
315
|
+
source_sessions TEXT DEFAULT '[]',
|
|
316
|
+
linked_learnings TEXT DEFAULT '[]',
|
|
317
|
+
use_count INTEGER DEFAULT 0,
|
|
318
|
+
success_count INTEGER DEFAULT 0,
|
|
319
|
+
fail_count INTEGER DEFAULT 0,
|
|
320
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
321
|
+
last_used_at TEXT DEFAULT NULL,
|
|
322
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
323
|
+
)
|
|
324
|
+
""")
|
|
325
|
+
conn.execute("""
|
|
326
|
+
CREATE TABLE IF NOT EXISTS skill_usage (
|
|
327
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
328
|
+
skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
|
|
329
|
+
session_id TEXT DEFAULT '',
|
|
330
|
+
success INTEGER NOT NULL DEFAULT 1,
|
|
331
|
+
context TEXT DEFAULT '',
|
|
332
|
+
notes TEXT DEFAULT '',
|
|
333
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
334
|
+
)
|
|
335
|
+
""")
|
|
336
|
+
_migrate_add_index(conn, "idx_skills_level", "skills", "level")
|
|
337
|
+
_migrate_add_index(conn, "idx_skills_trust", "skills", "trust_score")
|
|
338
|
+
_migrate_add_index(conn, "idx_skills_last_used", "skills", "last_used_at")
|
|
339
|
+
_migrate_add_index(conn, "idx_skill_usage_skill_id", "skill_usage", "skill_id")
|
|
340
|
+
_migrate_add_index(conn, "idx_skill_usage_created", "skill_usage", "created_at")
|
|
341
|
+
|
|
342
|
+
|
|
298
343
|
# Migration registry — APPEND ONLY, never reorder or delete
|
|
344
|
+
def _m17_cron_runs(conn):
|
|
345
|
+
conn.execute("""
|
|
346
|
+
CREATE TABLE IF NOT EXISTS cron_runs (
|
|
347
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
348
|
+
cron_id TEXT NOT NULL,
|
|
349
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
350
|
+
ended_at TEXT,
|
|
351
|
+
exit_code INTEGER,
|
|
352
|
+
summary TEXT DEFAULT '',
|
|
353
|
+
error TEXT DEFAULT '',
|
|
354
|
+
duration_secs REAL
|
|
355
|
+
)
|
|
356
|
+
""")
|
|
357
|
+
_migrate_add_index(conn, "idx_cron_runs_cron_id", "cron_runs", "cron_id")
|
|
358
|
+
_migrate_add_index(conn, "idx_cron_runs_started", "cron_runs", "started_at")
|
|
359
|
+
|
|
360
|
+
|
|
299
361
|
MIGRATIONS = [
|
|
300
362
|
(1, "learnings_columns", _m1_learnings_columns),
|
|
301
363
|
(2, "followups_reasoning", _m2_followups_reasoning),
|
|
@@ -312,6 +374,8 @@ MIGRATIONS = [
|
|
|
312
374
|
(13, "claude_session_id", _m13_claude_session_id),
|
|
313
375
|
(14, "learnings_priority_weight", _m14_learnings_priority_weight),
|
|
314
376
|
(15, "core_rules_tables", _m15_core_rules_tables),
|
|
377
|
+
(16, "skills_tables", _m16_skills_tables),
|
|
378
|
+
(17, "cron_runs", _m17_cron_runs),
|
|
315
379
|
]
|
|
316
380
|
|
|
317
381
|
|
package/src/db/_sessions.py
CHANGED