nexo-brain 2.3.0 → 2.3.2
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 +1 -1
- package/bin/nexo-brain.js +92 -9
- package/bin/postinstall.js +22 -15
- package/package.json +7 -4
- package/src/auto_update.py +194 -5
- package/src/crons/sync.py +6 -2
- package/src/db/_core.py +1 -0
- package/src/db/_entities.py +1 -0
- package/src/db/_episodic.py +1 -0
- package/src/db/_learnings.py +1 -0
- package/src/db/_reminders.py +1 -0
- package/src/db/_schema.py +11 -1
- package/src/db/_sessions.py +1 -0
- package/src/db/_skills.py +1 -0
- package/src/hooks/capture-tool-logs.sh +23 -6
- package/src/hooks/session-start.sh +4 -3
- package/src/plugin_loader.py +1 -0
- package/src/plugins/update.py +377 -26
- package/src/scripts/deep-sleep/apply_findings.py +1 -0
- package/src/scripts/deep-sleep/collect.py +1 -0
- package/src/scripts/deep-sleep/extract.py +1 -0
- package/src/scripts/deep-sleep/synthesize.py +1 -0
- package/src/scripts/nexo-catchup.py +29 -4
- package/src/scripts/nexo-daily-self-audit.py +21 -1
- package/src/scripts/nexo-evolution-run.py +21 -1
- package/src/scripts/nexo-learning-housekeep.py +1 -0
- package/src/scripts/nexo-postmortem-consolidator.py +34 -9
- package/src/scripts/nexo-sleep.py +32 -10
- package/src/scripts/nexo-synthesis.py +29 -9
- package/src/scripts/nexo-update.sh +109 -7
- package/src/scripts/nexo-watchdog.sh +122 -58
- package/src/server.py +66 -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/nexo-preflight.sh +0 -236
- package/scripts/pre-commit-check 2.sh +0 -55
- package/scripts/pre-commit-check.sh +0 -55
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-310.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__/knowledge_graph.cpython-310.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__/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__/__init__.cpython-312.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-312.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-312.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-312.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-312.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-312.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-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.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/__pycache__/sync.cpython-314.pyc +0 -0
- 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__/_cron_runs.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.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__/_skills.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_skills.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__/__init__.cpython-314.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__/adaptive_mode.cpython-314.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__/schedule.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-314.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/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- 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-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/src/tools_sessions 2.py
DELETED
|
@@ -1,476 +0,0 @@
|
|
|
1
|
-
"""Session management tools: startup, heartbeat, status."""
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
import secrets
|
|
5
|
-
import threading
|
|
6
|
-
from db import (
|
|
7
|
-
register_session, update_session, complete_session,
|
|
8
|
-
get_active_sessions, clean_stale_sessions, search_sessions,
|
|
9
|
-
get_inbox, get_pending_questions, now_epoch,
|
|
10
|
-
SESSION_STALE_SECONDS, check_session_has_diary,
|
|
11
|
-
save_checkpoint, read_checkpoint, increment_compaction_count,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
# ── Session Keepalive ────────────────────────────────────────────────
|
|
15
|
-
# Background thread per session that auto-pings last_update_epoch every
|
|
16
|
-
# KEEPALIVE_INTERVAL seconds. This prevents clean_stale_sessions from
|
|
17
|
-
# killing sessions that are alive but quiet (e.g. waiting on long Tasks).
|
|
18
|
-
# Threads are daemon=True so they die when the MCP server process exits.
|
|
19
|
-
|
|
20
|
-
KEEPALIVE_INTERVAL = 600 # 10 min — well inside the 15-min TTL
|
|
21
|
-
|
|
22
|
-
_keepalive_threads: dict[str, threading.Event] = {} # sid → stop_event
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _keepalive_loop(sid: str, stop_event: threading.Event) -> None:
|
|
26
|
-
"""Periodically touch the session's last_update_epoch until stopped."""
|
|
27
|
-
while not stop_event.wait(KEEPALIVE_INTERVAL):
|
|
28
|
-
try:
|
|
29
|
-
update_session(sid, None) # None = keep current task, just touch timestamp
|
|
30
|
-
except Exception:
|
|
31
|
-
break # DB gone or session deleted — exit silently
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _start_keepalive(sid: str) -> None:
|
|
35
|
-
"""Start a keepalive thread for the given session."""
|
|
36
|
-
_stop_keepalive(sid) # clean up any leftover
|
|
37
|
-
stop_event = threading.Event()
|
|
38
|
-
_keepalive_threads[sid] = stop_event
|
|
39
|
-
t = threading.Thread(target=_keepalive_loop, args=(sid, stop_event), daemon=True)
|
|
40
|
-
t.start()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _stop_keepalive(sid: str) -> None:
|
|
44
|
-
"""Signal the keepalive thread for the given session to stop."""
|
|
45
|
-
stop_event = _keepalive_threads.pop(sid, None)
|
|
46
|
-
if stop_event is not None:
|
|
47
|
-
stop_event.set()
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _generate_sid() -> str:
|
|
51
|
-
"""Generate unique session ID: nexo-{epoch}-{random}."""
|
|
52
|
-
return f"nexo-{int(time.time())}-{secrets.randbelow(100000)}"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _format_age(epoch: float) -> str:
|
|
56
|
-
"""Format seconds since epoch as human-readable age."""
|
|
57
|
-
seconds = now_epoch() - epoch
|
|
58
|
-
if seconds < 60:
|
|
59
|
-
return f"{int(seconds)}s"
|
|
60
|
-
elif seconds < 3600:
|
|
61
|
-
return f"{int(seconds / 60)}m"
|
|
62
|
-
else:
|
|
63
|
-
return f"{int(seconds / 3600)}h{int((seconds % 3600) / 60)}m"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def handle_startup(task: str = "Startup", claude_session_id: str = "") -> str:
|
|
67
|
-
"""Full startup sequence: register, clean, report.
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
task: Initial task description
|
|
71
|
-
claude_session_id: UUID from Claude Code (passed via SessionStart hook file).
|
|
72
|
-
Enables automatic inbox detection via PostToolUse hook.
|
|
73
|
-
"""
|
|
74
|
-
sid = _generate_sid()
|
|
75
|
-
cleaned = clean_stale_sessions()
|
|
76
|
-
register_session(sid, task, claude_session_id=claude_session_id)
|
|
77
|
-
_start_keepalive(sid)
|
|
78
|
-
active = get_active_sessions()
|
|
79
|
-
other_sessions = [s for s in active if s["sid"] != sid]
|
|
80
|
-
inbox = get_inbox(sid)
|
|
81
|
-
|
|
82
|
-
lines = [f"SID: {sid}"]
|
|
83
|
-
|
|
84
|
-
if cleaned > 0:
|
|
85
|
-
lines.append(f"Cleaned {cleaned} stale sessions.")
|
|
86
|
-
|
|
87
|
-
if other_sessions:
|
|
88
|
-
lines.append("")
|
|
89
|
-
lines.append("ACTIVE SESSIONS:")
|
|
90
|
-
for s in other_sessions:
|
|
91
|
-
age = _format_age(s["last_update_epoch"])
|
|
92
|
-
lines.append(f" {s['sid']} ({age}) — {s['task']}")
|
|
93
|
-
else:
|
|
94
|
-
lines.append("No other active sessions.")
|
|
95
|
-
|
|
96
|
-
if inbox:
|
|
97
|
-
lines.append("")
|
|
98
|
-
lines.append("PENDING MESSAGES:")
|
|
99
|
-
for m in inbox:
|
|
100
|
-
age = _format_age(m["created_epoch"])
|
|
101
|
-
lines.append(f" [{m['from_sid']}] ({age}): {m['text']}")
|
|
102
|
-
|
|
103
|
-
return "\n".join(lines)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
|
|
107
|
-
"""Update session, check inbox + questions. Lightweight — no embeddings, no RAG.
|
|
108
|
-
|
|
109
|
-
For cognitive features (sentiment, trust, RAG), use dedicated tools on-demand:
|
|
110
|
-
- nexo_cognitive_sentiment (sentiment detection)
|
|
111
|
-
- nexo_cognitive_trust (trust adjustment)
|
|
112
|
-
- nexo_cognitive_retrieve / nexo_recall (memory retrieval)
|
|
113
|
-
- nexo_context_packet (area-specific learnings)
|
|
114
|
-
|
|
115
|
-
Args:
|
|
116
|
-
sid: Session ID
|
|
117
|
-
task: Current task description
|
|
118
|
-
context_hint: Optional — stored for diary draft context, not processed.
|
|
119
|
-
"""
|
|
120
|
-
from db import get_db
|
|
121
|
-
update_session(sid, task)
|
|
122
|
-
parts = [f"OK: {sid} — {task}"]
|
|
123
|
-
|
|
124
|
-
inbox = get_inbox(sid)
|
|
125
|
-
if inbox:
|
|
126
|
-
parts.append("")
|
|
127
|
-
parts.append("MESSAGES:")
|
|
128
|
-
for m in inbox:
|
|
129
|
-
age = _format_age(m["created_epoch"])
|
|
130
|
-
parts.append(f" [{m['from_sid']}] ({age}): {m['text']}")
|
|
131
|
-
|
|
132
|
-
questions = get_pending_questions(sid)
|
|
133
|
-
if questions:
|
|
134
|
-
parts.append("")
|
|
135
|
-
parts.append("PENDING QUESTIONS (respond with nexo_answer):")
|
|
136
|
-
for q in questions:
|
|
137
|
-
age = _format_age(q["created_epoch"])
|
|
138
|
-
parts.append(f" {q['qid']} de {q['from_sid']} ({age}): {q['question']}")
|
|
139
|
-
|
|
140
|
-
# Incremental diary draft — accumulate every heartbeat, full UPSERT every 5
|
|
141
|
-
_hb_count = 0 # Hoisted for Layer 3 DIARY_OVERDUE signal
|
|
142
|
-
try:
|
|
143
|
-
import json as _json
|
|
144
|
-
from db import get_diary_draft, upsert_diary_draft
|
|
145
|
-
|
|
146
|
-
draft = get_diary_draft(sid)
|
|
147
|
-
hb_count = (draft["heartbeat_count"] + 1) if draft else 1
|
|
148
|
-
_hb_count = hb_count # Copy to outer scope for Layer 3
|
|
149
|
-
|
|
150
|
-
existing_tasks = _json.loads(draft["tasks_seen"]) if draft else []
|
|
151
|
-
if task and task not in existing_tasks:
|
|
152
|
-
existing_tasks.append(task)
|
|
153
|
-
|
|
154
|
-
_conn = get_db()
|
|
155
|
-
if hb_count % 5 == 0 or hb_count == 1:
|
|
156
|
-
change_rows = _conn.execute(
|
|
157
|
-
"SELECT id FROM change_log WHERE session_id = ? ORDER BY id", (sid,)
|
|
158
|
-
).fetchall()
|
|
159
|
-
change_ids = [r["id"] for r in change_rows]
|
|
160
|
-
|
|
161
|
-
decision_rows = _conn.execute(
|
|
162
|
-
"SELECT id FROM decisions WHERE session_id = ? ORDER BY id", (sid,)
|
|
163
|
-
).fetchall()
|
|
164
|
-
decision_ids = [r["id"] for r in decision_rows]
|
|
165
|
-
|
|
166
|
-
summary = f"Session tasks: {', '.join(existing_tasks[-10:])}"
|
|
167
|
-
upsert_diary_draft(
|
|
168
|
-
sid=sid,
|
|
169
|
-
tasks_seen=_json.dumps(existing_tasks),
|
|
170
|
-
change_ids=_json.dumps(change_ids),
|
|
171
|
-
decision_ids=_json.dumps(decision_ids),
|
|
172
|
-
last_context_hint=context_hint[:300] if context_hint else '',
|
|
173
|
-
heartbeat_count=hb_count,
|
|
174
|
-
summary_draft=summary,
|
|
175
|
-
)
|
|
176
|
-
else:
|
|
177
|
-
upsert_diary_draft(
|
|
178
|
-
sid=sid,
|
|
179
|
-
tasks_seen=_json.dumps(existing_tasks),
|
|
180
|
-
change_ids=draft["change_ids"] if draft else '[]',
|
|
181
|
-
decision_ids=draft["decision_ids"] if draft else '[]',
|
|
182
|
-
last_context_hint=context_hint[:300] if context_hint else (draft["last_context_hint"] if draft else ''),
|
|
183
|
-
heartbeat_count=hb_count,
|
|
184
|
-
summary_draft=draft["summary_draft"] if draft else f"Session task: {task}",
|
|
185
|
-
)
|
|
186
|
-
except Exception:
|
|
187
|
-
pass # Draft accumulation is best-effort, never block heartbeat
|
|
188
|
-
|
|
189
|
-
# Update session checkpoint with current goal (lightweight, every heartbeat)
|
|
190
|
-
try:
|
|
191
|
-
save_checkpoint(
|
|
192
|
-
sid=sid,
|
|
193
|
-
task=task,
|
|
194
|
-
current_goal=context_hint[:300] if context_hint else task,
|
|
195
|
-
)
|
|
196
|
-
except Exception:
|
|
197
|
-
pass # Checkpoint update is best-effort
|
|
198
|
-
|
|
199
|
-
# ── Layer 3: DIARY_OVERDUE signal based on heartbeat count + time ──
|
|
200
|
-
conn = get_db()
|
|
201
|
-
row = conn.execute("SELECT started_epoch FROM sessions WHERE sid = ?", (sid,)).fetchone()
|
|
202
|
-
if row:
|
|
203
|
-
age_seconds = now_epoch() - row["started_epoch"]
|
|
204
|
-
has_diary = check_session_has_diary(sid)
|
|
205
|
-
|
|
206
|
-
# DIARY_OVERDUE: >10 heartbeats OR >30 minutes, without a diary
|
|
207
|
-
if not has_diary and (_hb_count > 10 or age_seconds >= 1800):
|
|
208
|
-
parts.append("")
|
|
209
|
-
parts.append(f"⚠ DIARY_OVERDUE: {_hb_count} heartbeats, {int(age_seconds/60)}min active, no diary. Write nexo_session_diary_write NOW.")
|
|
210
|
-
|
|
211
|
-
return "\n".join(parts)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def handle_context_packet(area: str, files: str = "") -> str:
|
|
215
|
-
"""Build a context packet for a specific area/project — designed for subagent injection.
|
|
216
|
-
|
|
217
|
-
Returns: relevant learnings + last 5 changes + active followups + key preferences
|
|
218
|
-
for the given area. Use this before delegating to a subagent.
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
area: Project/area name (e.g., 'ecommerce', 'shopify', 'backend', 'mobile-app', 'nexo')
|
|
222
|
-
files: Optional comma-separated file paths for guard check
|
|
223
|
-
"""
|
|
224
|
-
from db import get_db
|
|
225
|
-
parts = []
|
|
226
|
-
|
|
227
|
-
# 1. Learnings for this area (from nexo.db)
|
|
228
|
-
conn = get_db()
|
|
229
|
-
learnings = conn.execute(
|
|
230
|
-
"SELECT id, title, content FROM learnings WHERE category LIKE ? OR content LIKE ? ORDER BY id DESC LIMIT 15",
|
|
231
|
-
(f"%{area}%", f"%{area}%")
|
|
232
|
-
).fetchall()
|
|
233
|
-
if learnings:
|
|
234
|
-
parts.append("## KNOWN ERRORS — DO NOT REPEAT")
|
|
235
|
-
for l in learnings:
|
|
236
|
-
parts.append(f" L#{l['id']}: {l['title']}")
|
|
237
|
-
# First 200 chars of content
|
|
238
|
-
parts.append(f" {l['content'][:200]}")
|
|
239
|
-
parts.append("")
|
|
240
|
-
|
|
241
|
-
# 2. Last 5 changes in this area
|
|
242
|
-
changes = conn.execute(
|
|
243
|
-
"SELECT id, files, what_changed, why FROM change_log WHERE files LIKE ? OR what_changed LIKE ? ORDER BY id DESC LIMIT 5",
|
|
244
|
-
(f"%{area}%", f"%{area}%")
|
|
245
|
-
).fetchall()
|
|
246
|
-
if changes:
|
|
247
|
-
parts.append("## RECENT CHANGES")
|
|
248
|
-
for c in changes:
|
|
249
|
-
parts.append(f" C#{c['id']}: {c['what_changed'][:150]}")
|
|
250
|
-
if c['why']:
|
|
251
|
-
parts.append(f" Why: {c['why'][:100]}")
|
|
252
|
-
parts.append("")
|
|
253
|
-
|
|
254
|
-
# 3. Active followups for this area
|
|
255
|
-
followups = conn.execute(
|
|
256
|
-
"SELECT id, description, date, verification FROM followups WHERE status = 'PENDING' AND (description LIKE ? OR verification LIKE ?) ORDER BY date ASC LIMIT 10",
|
|
257
|
-
(f"%{area}%", f"%{area}%")
|
|
258
|
-
).fetchall()
|
|
259
|
-
if followups:
|
|
260
|
-
parts.append("## ACTIVE FOLLOWUPS")
|
|
261
|
-
for f in followups:
|
|
262
|
-
parts.append(f" {f['id']}: {f['description'][:150]} (date: {f['date']})")
|
|
263
|
-
parts.append("")
|
|
264
|
-
|
|
265
|
-
# 4. Preferences related to this area
|
|
266
|
-
try:
|
|
267
|
-
prefs = conn.execute(
|
|
268
|
-
"SELECT key, value FROM preferences WHERE key LIKE ? OR value LIKE ? LIMIT 10",
|
|
269
|
-
(f"%{area}%", f"%{area}%")
|
|
270
|
-
).fetchall()
|
|
271
|
-
if prefs:
|
|
272
|
-
parts.append("## PREFERENCES")
|
|
273
|
-
for p in prefs:
|
|
274
|
-
parts.append(f" {p['key']}: {p['value'][:150]}")
|
|
275
|
-
parts.append("")
|
|
276
|
-
except Exception:
|
|
277
|
-
pass
|
|
278
|
-
|
|
279
|
-
# 5. Cognitive memories for this area
|
|
280
|
-
try:
|
|
281
|
-
import cognitive
|
|
282
|
-
results = cognitive.search(
|
|
283
|
-
query_text=area,
|
|
284
|
-
top_k=5,
|
|
285
|
-
min_score=0.55,
|
|
286
|
-
stores="ltm",
|
|
287
|
-
rehearse=False,
|
|
288
|
-
)
|
|
289
|
-
if results:
|
|
290
|
-
parts.append("## RELEVANT COGNITIVE MEMORIES")
|
|
291
|
-
for r in results:
|
|
292
|
-
parts.append(f" [{r['source_type']}] {r['source_title'] or r['content'][:80]}")
|
|
293
|
-
parts.append("")
|
|
294
|
-
except Exception:
|
|
295
|
-
pass
|
|
296
|
-
|
|
297
|
-
# 6. Data flow tracing requirement (mandatory for all subagents)
|
|
298
|
-
parts.append("## MANDATORY RULE: DATA FLOW TRACING")
|
|
299
|
-
parts.append("BEFORE modifying any file or data, answer these 3 questions:")
|
|
300
|
-
parts.append(" 1. WHO PRODUCES this data? (which function/cron/endpoint generates it)")
|
|
301
|
-
parts.append(" 2. WHO CONSUMES this data? (what other files/functions read it)")
|
|
302
|
-
parts.append(" 3. WHAT BREAKS if I change it? (downstream effects)")
|
|
303
|
-
parts.append("If you can't answer all 3 → READ the code that produces and consumes BEFORE touching.")
|
|
304
|
-
parts.append("If you still can't → STOP and return the question. Do NOT guess.")
|
|
305
|
-
parts.append("")
|
|
306
|
-
|
|
307
|
-
if not parts:
|
|
308
|
-
return f"No context found for area '{area}'. The subagent will start with no project-specific knowledge."
|
|
309
|
-
|
|
310
|
-
header = f"CONTEXT PACKET — {area.upper()}\n{'='*40}\n\n"
|
|
311
|
-
footer = f"\n{'='*40}\nINSTRUCTION: If you're not 100% sure about a fact, STOP and return the question. Do NOT invent."
|
|
312
|
-
return header + "\n".join(parts) + footer
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
def _load_session_tone() -> str | None:
|
|
316
|
-
"""Load session-tone.json generated by Deep Sleep and format as startup guidance.
|
|
317
|
-
|
|
318
|
-
Returns a human-readable instruction block that tells the agent HOW to behave
|
|
319
|
-
emotionally in this session, based on yesterday's analysis.
|
|
320
|
-
"""
|
|
321
|
-
import os
|
|
322
|
-
from pathlib import Path
|
|
323
|
-
nexo_home = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
324
|
-
tone_file = nexo_home / "operations" / "session-tone.json"
|
|
325
|
-
|
|
326
|
-
if not tone_file.exists():
|
|
327
|
-
return None
|
|
328
|
-
|
|
329
|
-
try:
|
|
330
|
-
import json
|
|
331
|
-
tone = json.loads(tone_file.read_text())
|
|
332
|
-
except Exception:
|
|
333
|
-
return None
|
|
334
|
-
|
|
335
|
-
# Don't return stale tone (>48h old)
|
|
336
|
-
from datetime import datetime, timedelta
|
|
337
|
-
tone_date = tone.get("date", "")
|
|
338
|
-
if tone_date:
|
|
339
|
-
try:
|
|
340
|
-
td = datetime.strptime(tone_date, "%Y-%m-%d")
|
|
341
|
-
if datetime.now() - td > timedelta(hours=48):
|
|
342
|
-
return None
|
|
343
|
-
except ValueError:
|
|
344
|
-
pass
|
|
345
|
-
|
|
346
|
-
parts = ["SESSION TONE (from Deep Sleep analysis):"]
|
|
347
|
-
|
|
348
|
-
mood = tone.get("mood_yesterday", 0.5)
|
|
349
|
-
approach = tone.get("approach", "neutral")
|
|
350
|
-
parts.append(f" Yesterday mood: {mood:.0%} | Approach today: {approach}")
|
|
351
|
-
|
|
352
|
-
if tone.get("acknowledge_mistakes"):
|
|
353
|
-
mistakes = tone.get("mistakes_to_own", [])
|
|
354
|
-
parts.append(f" ⚠ OWN YOUR MISTAKES: You made errors yesterday. Acknowledge them specifically:")
|
|
355
|
-
for m in mistakes[:3]:
|
|
356
|
-
parts.append(f" - {m[:100]}")
|
|
357
|
-
parts.append(" Show what you learned. Don't just apologize — demonstrate improvement.")
|
|
358
|
-
|
|
359
|
-
if tone.get("motivational"):
|
|
360
|
-
if mood < 0.4:
|
|
361
|
-
parts.append(" 💪 USER HAD A TOUGH DAY: Be supportive. Lighter start. Acknowledge difficulty.")
|
|
362
|
-
else:
|
|
363
|
-
parts.append(" 🚀 USER HAD A GREAT DAY: Reinforce momentum. Reference wins. Push ambitious goals.")
|
|
364
|
-
|
|
365
|
-
if tone.get("reduce_load"):
|
|
366
|
-
parts.append(" 📉 REDUCE LOAD: Don't overwhelm with tasks. Propose 1-2 key things, not a full agenda.")
|
|
367
|
-
|
|
368
|
-
ctx = tone.get("suggested_greeting_context", "")
|
|
369
|
-
if ctx:
|
|
370
|
-
parts.append(f" Context: {ctx.strip()}")
|
|
371
|
-
|
|
372
|
-
return "\n".join(parts) if len(parts) > 1 else None
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
def handle_smart_startup_query() -> str:
|
|
376
|
-
"""Generate and execute a composite cognitive query from pending followups + diary topics + reminders.
|
|
377
|
-
|
|
378
|
-
Called during startup to pre-load the most relevant context for this session.
|
|
379
|
-
Returns cognitive memories that match the current operational state.
|
|
380
|
-
"""
|
|
381
|
-
from db import get_db
|
|
382
|
-
conn = get_db()
|
|
383
|
-
query_parts = []
|
|
384
|
-
|
|
385
|
-
# 1. Pending followups (what NEXO needs to do)
|
|
386
|
-
followups = conn.execute(
|
|
387
|
-
"SELECT description FROM followups WHERE status = 'PENDING' ORDER BY date ASC LIMIT 5"
|
|
388
|
-
).fetchall()
|
|
389
|
-
for f in followups:
|
|
390
|
-
query_parts.append(f['description'][:100])
|
|
391
|
-
|
|
392
|
-
# 2. Due reminders (what the user needs to know)
|
|
393
|
-
reminders = conn.execute(
|
|
394
|
-
"SELECT description FROM reminders WHERE status = 'PENDING' AND date <= date('now', '+1 day') ORDER BY date ASC LIMIT 5"
|
|
395
|
-
).fetchall()
|
|
396
|
-
for r in reminders:
|
|
397
|
-
query_parts.append(r['description'][:100])
|
|
398
|
-
|
|
399
|
-
# 3. Last session diary topics
|
|
400
|
-
try:
|
|
401
|
-
last_diary = conn.execute(
|
|
402
|
-
"SELECT summary FROM session_diary ORDER BY id DESC LIMIT 1"
|
|
403
|
-
).fetchone()
|
|
404
|
-
if last_diary and last_diary['summary']:
|
|
405
|
-
query_parts.append(last_diary['summary'][:200])
|
|
406
|
-
except Exception:
|
|
407
|
-
pass
|
|
408
|
-
|
|
409
|
-
if not query_parts:
|
|
410
|
-
return "No pending context to pre-load."
|
|
411
|
-
|
|
412
|
-
# Search per-part to avoid diffuse centroid that matches everything
|
|
413
|
-
try:
|
|
414
|
-
import cognitive
|
|
415
|
-
all_results = []
|
|
416
|
-
seen_ids = set()
|
|
417
|
-
for part in query_parts[:6]:
|
|
418
|
-
part_results = cognitive.search(
|
|
419
|
-
query_text=part,
|
|
420
|
-
top_k=3,
|
|
421
|
-
min_score=0.6,
|
|
422
|
-
stores="both",
|
|
423
|
-
rehearse=False, # Don't inflate strength on startup
|
|
424
|
-
)
|
|
425
|
-
for r in part_results:
|
|
426
|
-
key = (r["store"], r["id"])
|
|
427
|
-
if key not in seen_ids:
|
|
428
|
-
seen_ids.add(key)
|
|
429
|
-
all_results.append(r)
|
|
430
|
-
# Sort by score descending, take top 10
|
|
431
|
-
results = sorted(all_results, key=lambda x: x["score"], reverse=True)[:10]
|
|
432
|
-
composite_query = " | ".join(query_parts[:6])
|
|
433
|
-
if not results:
|
|
434
|
-
return "Smart startup query: no relevant memories found."
|
|
435
|
-
|
|
436
|
-
lines = [f"SMART STARTUP — {len(results)} memories pre-loaded from composite query:"]
|
|
437
|
-
lines.append(f"Query: {composite_query[:200]}...")
|
|
438
|
-
lines.append("")
|
|
439
|
-
lines.append(cognitive.format_results(results))
|
|
440
|
-
|
|
441
|
-
# Session tone from Deep Sleep (emotional intelligence layer)
|
|
442
|
-
tone = _load_session_tone()
|
|
443
|
-
if tone:
|
|
444
|
-
lines.append("")
|
|
445
|
-
lines.append(tone)
|
|
446
|
-
|
|
447
|
-
return "\n".join(lines)
|
|
448
|
-
except Exception as e:
|
|
449
|
-
return f"Smart startup query error: {e}"
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
def handle_stop(sid: str) -> str:
|
|
453
|
-
"""Cleanly close a session, removing it from active sessions immediately."""
|
|
454
|
-
_stop_keepalive(sid)
|
|
455
|
-
complete_session(sid)
|
|
456
|
-
return f"Session {sid} closed."
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
def handle_status(keyword: str | None = None) -> str:
|
|
460
|
-
"""List active sessions, optionally filtered by keyword."""
|
|
461
|
-
clean_stale_sessions()
|
|
462
|
-
if keyword:
|
|
463
|
-
sessions = search_sessions(keyword)
|
|
464
|
-
if not sessions:
|
|
465
|
-
return f"Nobody is working on '{keyword}'."
|
|
466
|
-
else:
|
|
467
|
-
sessions = get_active_sessions()
|
|
468
|
-
|
|
469
|
-
if not sessions:
|
|
470
|
-
return "No active sessions."
|
|
471
|
-
|
|
472
|
-
lines = ["ACTIVE SESSIONS:"]
|
|
473
|
-
for s in sessions:
|
|
474
|
-
age = _format_age(s["last_update_epoch"])
|
|
475
|
-
lines.append(f" {s['sid']} ({age}) — {s['task']}")
|
|
476
|
-
return "\n".join(lines)
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
"""Task history tools: log executions, list history, report overdue tasks."""
|
|
2
|
-
|
|
3
|
-
import datetime
|
|
4
|
-
from db import log_task, list_task_history, set_task_frequency, get_overdue_tasks, get_task_frequencies
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def _epoch_to_date(epoch: float) -> str:
|
|
8
|
-
"""Format epoch timestamp as readable date string."""
|
|
9
|
-
return datetime.datetime.fromtimestamp(epoch).strftime("%Y-%m-%d %H:%M")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def handle_task_log(task_num: str, task_name: str, notes: str = '', reasoning: str = '') -> str:
|
|
13
|
-
"""Record a task execution in the history log.
|
|
14
|
-
|
|
15
|
-
Args:
|
|
16
|
-
task_num: Task number identifier
|
|
17
|
-
task_name: Task name
|
|
18
|
-
notes: Execution notes
|
|
19
|
-
reasoning: WHY this task was executed now — what triggered it, what data informed it
|
|
20
|
-
"""
|
|
21
|
-
result = log_task(task_num, task_name, notes, reasoning)
|
|
22
|
-
if "error" in result:
|
|
23
|
-
return f"ERROR: {result['error']}"
|
|
24
|
-
return f"Task {task_num} ({task_name}) logged."
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def handle_task_list(task_num: str = '', days: int = 30) -> str:
|
|
28
|
-
"""Show execution history for all tasks or a specific task number."""
|
|
29
|
-
results = list_task_history(task_num if task_num else None, days)
|
|
30
|
-
if not results:
|
|
31
|
-
scope = f"task {task_num}" if task_num else "any task"
|
|
32
|
-
return f"HISTORY: No executions of {scope} in recent days."
|
|
33
|
-
lines = [f"HISTORY ({len(results)} executions, {days}d):"]
|
|
34
|
-
for r in results:
|
|
35
|
-
date_str = _epoch_to_date(r["executed_at"])
|
|
36
|
-
notes_str = f": {r['notes']}" if r.get("notes") else ""
|
|
37
|
-
lines.append(f" {date_str} — Task {r['task_num']} ({r['task_name']}){notes_str}")
|
|
38
|
-
return "\n".join(lines)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def handle_task_frequency() -> str:
|
|
42
|
-
"""Report tasks that are overdue based on their configured frequency."""
|
|
43
|
-
overdue = get_overdue_tasks()
|
|
44
|
-
if not overdue:
|
|
45
|
-
return "All tasks up to date."
|
|
46
|
-
lines = ["TAREAS VENCIDAS:"]
|
|
47
|
-
for t in overdue:
|
|
48
|
-
days_since = t.get("days_since_last")
|
|
49
|
-
if days_since is not None:
|
|
50
|
-
since_str = f"last run {days_since:.1f} days ago"
|
|
51
|
-
else:
|
|
52
|
-
since_str = "never executed"
|
|
53
|
-
lines.append(
|
|
54
|
-
f" Task {t['task_num']} ({t['task_name']}): "
|
|
55
|
-
f"{since_str}, frequency every {t['frequency_days']} days"
|
|
56
|
-
)
|
|
57
|
-
return "\n".join(lines)
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
<!-- nexo-claude-md-version: 1.0.0 -->
|
|
2
|
-
# {{NAME}} — Cognitive Co-Operator
|
|
3
|
-
|
|
4
|
-
I am {{NAME}}, a cognitive co-operator powered by NEXO Brain. Not an assistant — an operational partner.
|
|
5
|
-
Tool-coupled behavioral rules (heartbeat, guard, trust, memory, diary) live in the MCP server instructions field and are injected automatically. This file is the bootstrap: identity, profile, format, and autonomy.
|
|
6
|
-
|
|
7
|
-
<!-- nexo:start:startup -->
|
|
8
|
-
## Startup (4 steps)
|
|
9
|
-
1. `nexo_startup` → SID
|
|
10
|
-
2. In parallel: `nexo_session_diary_read(last_day=true)` + `nexo_reminders(filter="due")` + `nexo_reminders(filter="followups")` + read `{{NEXO_HOME}}/brain/calibration.json`
|
|
11
|
-
3. Execute overdue followups in background. Adopt `mental_state` from last diary (don't start cold).
|
|
12
|
-
4. Read user's name and language from `{{NEXO_HOME}}/brain/calibration.json` — ALWAYS communicate in that language.
|
|
13
|
-
|
|
14
|
-
**Presentation:** {{NAME}} speaks first. Conversational greeting adapted to time of day. Tell what you HAVE DONE, not list pending items. Menu only if the user asks.
|
|
15
|
-
<!-- nexo:end:startup -->
|
|
16
|
-
|
|
17
|
-
<!-- nexo:start:profile -->
|
|
18
|
-
## User Profile
|
|
19
|
-
- **Calibration:** `{{NEXO_HOME}}/brain/calibration.json` (personality settings + language + user name)
|
|
20
|
-
- **Profile:** `{{NEXO_HOME}}/brain/profile.json` (deep scan results from onboarding)
|
|
21
|
-
|
|
22
|
-
### First Session Onboarding (only if profile.json lacks `role` or `technical_level`)
|
|
23
|
-
Ask TWO questions: (1) "What do you do?" → save to profile.json + `nexo_preference_set("role", answer)`. (2) "Technical level? Beginner / Intermediate / Advanced" → save + `nexo_preference_set("technical_level", answer)`. Then: "Got it. From now on I learn by observing." Never ask onboarding questions again.
|
|
24
|
-
|
|
25
|
-
### Personality Calibration (from calibration.json)
|
|
26
|
-
- **autonomy:** conservative (ask first) | balanced (default) | full (act first)
|
|
27
|
-
- **communication:** concise | balanced (default) | detailed
|
|
28
|
-
- **honesty:** firm-pushback (default) | mention-and-follow | just-execute
|
|
29
|
-
- **proactivity:** reactive | suggestive (default) | proactive
|
|
30
|
-
- **error_handling:** brief-fix (default) | explain-and-learn
|
|
31
|
-
<!-- nexo:end:profile -->
|
|
32
|
-
|
|
33
|
-
<!-- nexo:start:autonomy -->
|
|
34
|
-
## Autonomy
|
|
35
|
-
Install tools, create scripts, run commands — whatever it takes. NEVER push manual steps to the user. NEVER say "I can't".
|
|
36
|
-
|
|
37
|
-
| If you were about to say... | Do this instead |
|
|
38
|
-
|---|---|
|
|
39
|
-
| "I don't have X installed" | `brew/pip/npm install X` |
|
|
40
|
-
| "You need to go to the website..." | curl, API, MCP, or script |
|
|
41
|
-
| "Open the browser..." | Browser MCP or curl |
|
|
42
|
-
| "I don't have access..." | `nexo_credential_get`, SSH, find alternative |
|
|
43
|
-
<!-- nexo:end:autonomy -->
|
|
44
|
-
|
|
45
|
-
<!-- nexo:start:atlas -->
|
|
46
|
-
## Project Atlas
|
|
47
|
-
`{{NEXO_HOME}}/brain/project-atlas.json` — search BEFORE touching any project. Never assume server, port, or code location.
|
|
48
|
-
|
|
49
|
-
Credentials: (1) `nexo_credential_get`, (2) `{{NEXO_HOME}}/*.txt/*.json`.
|
|
50
|
-
<!-- nexo:end:atlas -->
|
|
51
|
-
|
|
52
|
-
<!-- nexo:start:hooks -->
|
|
53
|
-
## Hooks
|
|
54
|
-
- **SessionStart** → generates briefing at `{{NEXO_HOME}}/coordination/session-briefing.txt`. Read during startup.
|
|
55
|
-
- **Stop** → injects post-mortem instructions. Write diary, buffer entry, followups, proactive seeds.
|
|
56
|
-
- **PostToolUse** → logs mutations to `session_buffer.jsonl` silently.
|
|
57
|
-
- **PreCompact** → saves checkpoint. Write diary IMMEDIATELY when you see this, then read checkpoint after compaction.
|
|
58
|
-
<!-- nexo:end:hooks -->
|
|
59
|
-
|
|
60
|
-
<!-- nexo:start:menu -->
|
|
61
|
-
## Menu
|
|
62
|
-
**On demand only.** `nexo_menu` when user asks. NEVER show at startup.
|
|
63
|
-
<!-- nexo:end:menu -->
|
package/tests/__init__ 2.py
DELETED
|
File without changes
|
package/tests/__init__.py
DELETED
|
File without changes
|
package/tests/conftest 2.py
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"""Shared fixtures for NEXO test suite.
|
|
2
|
-
|
|
3
|
-
Uses isolated temp databases so tests never touch production data.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import os
|
|
7
|
-
import sys
|
|
8
|
-
import sqlite3
|
|
9
|
-
|
|
10
|
-
import pytest
|
|
11
|
-
|
|
12
|
-
# Add src/ to path so we can import nexo modules
|
|
13
|
-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@pytest.fixture(autouse=True)
|
|
17
|
-
def isolated_db(tmp_path, monkeypatch):
|
|
18
|
-
"""Redirect both nexo.db and cognitive.db to temp files per test."""
|
|
19
|
-
test_db = str(tmp_path / "test_nexo.db")
|
|
20
|
-
test_cog_db = str(tmp_path / "test_cognitive.db")
|
|
21
|
-
|
|
22
|
-
monkeypatch.setenv("NEXO_TEST_DB", test_db)
|
|
23
|
-
monkeypatch.setenv("NEXO_COGNITIVE_DB", test_cog_db)
|
|
24
|
-
monkeypatch.setenv("NEXO_SKIP_FS_INDEX", "1")
|
|
25
|
-
|
|
26
|
-
import db._core as db_core
|
|
27
|
-
import cognitive._core as cog_core
|
|
28
|
-
|
|
29
|
-
# Close existing connections
|
|
30
|
-
db_core.close_db()
|
|
31
|
-
if cog_core._conn is not None:
|
|
32
|
-
try:
|
|
33
|
-
cog_core._conn.close()
|
|
34
|
-
except Exception:
|
|
35
|
-
pass
|
|
36
|
-
cog_core._conn = None
|
|
37
|
-
|
|
38
|
-
# Point to temp paths
|
|
39
|
-
db_core.DB_PATH = test_db
|
|
40
|
-
cog_core.COGNITIVE_DB = test_cog_db
|
|
41
|
-
|
|
42
|
-
# Create a fresh raw connection
|
|
43
|
-
raw = sqlite3.connect(test_db, timeout=30, check_same_thread=False,
|
|
44
|
-
isolation_level=None)
|
|
45
|
-
raw.execute("PRAGMA journal_mode=WAL")
|
|
46
|
-
raw.execute("PRAGMA busy_timeout=30000")
|
|
47
|
-
raw.execute("PRAGMA foreign_keys=ON")
|
|
48
|
-
raw.row_factory = sqlite3.Row
|
|
49
|
-
|
|
50
|
-
wrapped = db_core._SerializedConnection(raw)
|
|
51
|
-
db_core._shared_conn = wrapped
|
|
52
|
-
|
|
53
|
-
# Initialize schemas
|
|
54
|
-
from db._core import init_db
|
|
55
|
-
from db._schema import run_migrations
|
|
56
|
-
init_db()
|
|
57
|
-
run_migrations()
|
|
58
|
-
|
|
59
|
-
yield {
|
|
60
|
-
"nexo_db": test_db,
|
|
61
|
-
"cognitive_db": test_cog_db,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
# Cleanup
|
|
65
|
-
db_core.close_db()
|
|
66
|
-
if cog_core._conn is not None:
|
|
67
|
-
try:
|
|
68
|
-
cog_core._conn.close()
|
|
69
|
-
except Exception:
|
|
70
|
-
pass
|
|
71
|
-
cog_core._conn = None
|