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/db/_schema 2.py
DELETED
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
"""NEXO DB — Schema module."""
|
|
2
|
-
from db._core import get_db
|
|
3
|
-
from db._fts import _migrate_add_column, _migrate_add_index
|
|
4
|
-
|
|
5
|
-
# ── Formal Migration System ─────────────────────────────────────
|
|
6
|
-
#
|
|
7
|
-
# Each migration is (version, name, callable). Migrations run once
|
|
8
|
-
# and are tracked in schema_migrations. The version number MUST be
|
|
9
|
-
# strictly increasing. Add new migrations at the end of the list.
|
|
10
|
-
#
|
|
11
|
-
# For users upgrading via npm/git, init_db() calls run_migrations()
|
|
12
|
-
# automatically — no manual steps needed.
|
|
13
|
-
|
|
14
|
-
def _m1_learnings_columns(conn):
|
|
15
|
-
_migrate_add_column(conn, "learnings", "reasoning", "TEXT")
|
|
16
|
-
_migrate_add_column(conn, "learnings", "prevention", "TEXT DEFAULT ''")
|
|
17
|
-
_migrate_add_column(conn, "learnings", "applies_to", "TEXT DEFAULT ''")
|
|
18
|
-
_migrate_add_column(conn, "learnings", "status", "TEXT DEFAULT 'active'")
|
|
19
|
-
_migrate_add_column(conn, "learnings", "review_due_at", "REAL")
|
|
20
|
-
_migrate_add_column(conn, "learnings", "last_reviewed_at", "REAL")
|
|
21
|
-
|
|
22
|
-
def _m2_followups_reasoning(conn):
|
|
23
|
-
_migrate_add_column(conn, "followups", "reasoning", "TEXT")
|
|
24
|
-
_migrate_add_column(conn, "task_history", "reasoning", "TEXT")
|
|
25
|
-
|
|
26
|
-
def _m3_decisions_review(conn):
|
|
27
|
-
_migrate_add_column(conn, "decisions", "status", "TEXT DEFAULT 'pending_review'")
|
|
28
|
-
_migrate_add_column(conn, "decisions", "review_due_at", "TEXT")
|
|
29
|
-
_migrate_add_column(conn, "decisions", "last_reviewed_at", "TEXT")
|
|
30
|
-
_migrate_add_index(conn, "idx_decisions_domain", "decisions", "domain")
|
|
31
|
-
_migrate_add_index(conn, "idx_decisions_created", "decisions", "created_at")
|
|
32
|
-
_migrate_add_index(conn, "idx_decisions_review_due", "decisions", "review_due_at")
|
|
33
|
-
|
|
34
|
-
def _m4_session_diary_columns(conn):
|
|
35
|
-
_migrate_add_index(conn, "idx_session_diary_sid", "session_diary", "session_id")
|
|
36
|
-
_migrate_add_column(conn, "session_diary", "mental_state", "TEXT")
|
|
37
|
-
_migrate_add_column(conn, "session_diary", "domain", "TEXT")
|
|
38
|
-
_migrate_add_column(conn, "session_diary", "user_signals", "TEXT")
|
|
39
|
-
_migrate_add_column(conn, "session_diary", "self_critique", "TEXT")
|
|
40
|
-
|
|
41
|
-
def _m5_change_log_indexes(conn):
|
|
42
|
-
_migrate_add_index(conn, "idx_change_log_created", "change_log", "created_at")
|
|
43
|
-
_migrate_add_index(conn, "idx_change_log_files", "change_log", "files")
|
|
44
|
-
_migrate_add_index(conn, "idx_learnings_status", "learnings", "status")
|
|
45
|
-
_migrate_add_index(conn, "idx_learnings_review_due", "learnings", "review_due_at")
|
|
46
|
-
|
|
47
|
-
def _m6_error_guard_tables(conn):
|
|
48
|
-
conn.execute("""
|
|
49
|
-
CREATE TABLE IF NOT EXISTS error_repetitions (
|
|
50
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
51
|
-
new_learning_id INTEGER NOT NULL,
|
|
52
|
-
original_learning_id INTEGER NOT NULL,
|
|
53
|
-
similarity REAL NOT NULL,
|
|
54
|
-
area TEXT NOT NULL,
|
|
55
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
56
|
-
)
|
|
57
|
-
""")
|
|
58
|
-
conn.execute("""
|
|
59
|
-
CREATE TABLE IF NOT EXISTS guard_checks (
|
|
60
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
61
|
-
session_id TEXT,
|
|
62
|
-
files TEXT,
|
|
63
|
-
area TEXT,
|
|
64
|
-
learnings_returned INTEGER DEFAULT 0,
|
|
65
|
-
blocking_rules_returned INTEGER DEFAULT 0,
|
|
66
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
67
|
-
)
|
|
68
|
-
""")
|
|
69
|
-
_migrate_add_index(conn, "idx_error_repetitions_area", "error_repetitions", "area")
|
|
70
|
-
_migrate_add_index(conn, "idx_guard_checks_session", "guard_checks", "session_id")
|
|
71
|
-
|
|
72
|
-
def _m7_diary_source_and_draft(conn):
|
|
73
|
-
_migrate_add_column(conn, "session_diary", "source", "TEXT DEFAULT 'claude'")
|
|
74
|
-
conn.execute("""
|
|
75
|
-
CREATE TABLE IF NOT EXISTS session_diary_draft (
|
|
76
|
-
sid TEXT PRIMARY KEY,
|
|
77
|
-
summary_draft TEXT DEFAULT '',
|
|
78
|
-
tasks_seen TEXT DEFAULT '[]',
|
|
79
|
-
change_ids TEXT DEFAULT '[]',
|
|
80
|
-
decision_ids TEXT DEFAULT '[]',
|
|
81
|
-
last_context_hint TEXT DEFAULT '',
|
|
82
|
-
heartbeat_count INTEGER DEFAULT 0,
|
|
83
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
84
|
-
updated_at TEXT DEFAULT (datetime('now'))
|
|
85
|
-
)
|
|
86
|
-
""")
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def _m8_adaptive_log_and_somatic(conn):
|
|
90
|
-
conn.execute("""
|
|
91
|
-
CREATE TABLE IF NOT EXISTS adaptive_log (
|
|
92
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
93
|
-
timestamp TEXT DEFAULT (datetime('now')),
|
|
94
|
-
mode TEXT NOT NULL,
|
|
95
|
-
tension_score REAL NOT NULL,
|
|
96
|
-
sig_vibe REAL DEFAULT 0,
|
|
97
|
-
sig_corrections REAL DEFAULT 0,
|
|
98
|
-
sig_brevity REAL DEFAULT 0,
|
|
99
|
-
sig_topic REAL DEFAULT 0,
|
|
100
|
-
sig_tool_errors REAL DEFAULT 0,
|
|
101
|
-
sig_git_diff REAL DEFAULT 0,
|
|
102
|
-
context_hint TEXT DEFAULT '',
|
|
103
|
-
feedback_event TEXT DEFAULT NULL,
|
|
104
|
-
feedback_delta INTEGER DEFAULT NULL,
|
|
105
|
-
feedback_ts TEXT DEFAULT NULL
|
|
106
|
-
)
|
|
107
|
-
""")
|
|
108
|
-
conn.execute("CREATE INDEX IF NOT EXISTS idx_adaptive_log_ts ON adaptive_log(timestamp)")
|
|
109
|
-
conn.execute("""
|
|
110
|
-
CREATE TABLE IF NOT EXISTS somatic_events (
|
|
111
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
112
|
-
timestamp TEXT DEFAULT (datetime('now')),
|
|
113
|
-
target TEXT NOT NULL,
|
|
114
|
-
target_type TEXT NOT NULL,
|
|
115
|
-
event_type TEXT NOT NULL,
|
|
116
|
-
delta REAL NOT NULL,
|
|
117
|
-
source TEXT DEFAULT '',
|
|
118
|
-
projected INTEGER DEFAULT 0
|
|
119
|
-
)
|
|
120
|
-
""")
|
|
121
|
-
conn.execute("CREATE INDEX IF NOT EXISTS idx_somatic_events_target ON somatic_events(target)")
|
|
122
|
-
conn.execute("CREATE INDEX IF NOT EXISTS idx_somatic_events_projected ON somatic_events(projected)")
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _m11_artifact_registry(conn):
|
|
126
|
-
"""Artifact Registry — structured index of things NEXO creates/deploys.
|
|
127
|
-
|
|
128
|
-
Solves 'recent work amnesia': services, dashboards, scripts, APIs that
|
|
129
|
-
NEXO builds but can't find hours later because semantic search fails on
|
|
130
|
-
operational vocabulary mismatches (e.g., 'backend' vs 'FastAPI localhost:6174').
|
|
131
|
-
|
|
132
|
-
Design informed by 3-way AI debate (GPT-5.4 + Gemini 3.1 Pro + Claude Opus 4.6).
|
|
133
|
-
Key insight: operational facts need first-class structured storage, not just
|
|
134
|
-
vector embeddings buried in prose diaries.
|
|
135
|
-
"""
|
|
136
|
-
conn.execute("""
|
|
137
|
-
CREATE TABLE IF NOT EXISTS artifact_registry (
|
|
138
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
139
|
-
kind TEXT NOT NULL,
|
|
140
|
-
canonical_name TEXT NOT NULL,
|
|
141
|
-
aliases TEXT DEFAULT '[]',
|
|
142
|
-
description TEXT DEFAULT '',
|
|
143
|
-
uri TEXT DEFAULT '',
|
|
144
|
-
ports TEXT DEFAULT '[]',
|
|
145
|
-
paths TEXT DEFAULT '[]',
|
|
146
|
-
run_cmd TEXT DEFAULT '',
|
|
147
|
-
repo TEXT DEFAULT '',
|
|
148
|
-
domain TEXT DEFAULT '',
|
|
149
|
-
state TEXT DEFAULT 'active',
|
|
150
|
-
session_id TEXT DEFAULT '',
|
|
151
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
152
|
-
last_touched_at TEXT DEFAULT (datetime('now')),
|
|
153
|
-
last_verified_at TEXT DEFAULT NULL,
|
|
154
|
-
metadata TEXT DEFAULT '{}'
|
|
155
|
-
)
|
|
156
|
-
""")
|
|
157
|
-
conn.execute("""
|
|
158
|
-
CREATE TABLE IF NOT EXISTS artifact_aliases (
|
|
159
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
160
|
-
artifact_id INTEGER NOT NULL REFERENCES artifact_registry(id) ON DELETE CASCADE,
|
|
161
|
-
phrase TEXT NOT NULL,
|
|
162
|
-
source TEXT DEFAULT 'manual',
|
|
163
|
-
confidence REAL DEFAULT 1.0,
|
|
164
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
165
|
-
UNIQUE(artifact_id, phrase)
|
|
166
|
-
)
|
|
167
|
-
""")
|
|
168
|
-
conn.execute("CREATE INDEX IF NOT EXISTS idx_artifact_state ON artifact_registry(state)")
|
|
169
|
-
conn.execute("CREATE INDEX IF NOT EXISTS idx_artifact_kind ON artifact_registry(kind)")
|
|
170
|
-
conn.execute("CREATE INDEX IF NOT EXISTS idx_artifact_domain ON artifact_registry(domain)")
|
|
171
|
-
conn.execute("CREATE INDEX IF NOT EXISTS idx_artifact_last_touched ON artifact_registry(last_touched_at)")
|
|
172
|
-
conn.execute("CREATE INDEX IF NOT EXISTS idx_artifact_aliases_phrase ON artifact_aliases(phrase)")
|
|
173
|
-
conn.execute("CREATE INDEX IF NOT EXISTS idx_artifact_aliases_aid ON artifact_aliases(artifact_id)")
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def _m10_diary_archive(conn):
|
|
177
|
-
"""Permanent diary archive — diaries are never truly deleted, just moved here."""
|
|
178
|
-
conn.execute("""
|
|
179
|
-
CREATE TABLE IF NOT EXISTS diary_archive (
|
|
180
|
-
id INTEGER PRIMARY KEY,
|
|
181
|
-
session_id TEXT NOT NULL,
|
|
182
|
-
created_at TEXT NOT NULL,
|
|
183
|
-
decisions TEXT NOT NULL,
|
|
184
|
-
discarded TEXT,
|
|
185
|
-
pending TEXT,
|
|
186
|
-
context_next TEXT,
|
|
187
|
-
summary TEXT NOT NULL,
|
|
188
|
-
mental_state TEXT,
|
|
189
|
-
domain TEXT,
|
|
190
|
-
user_signals TEXT,
|
|
191
|
-
self_critique TEXT DEFAULT '',
|
|
192
|
-
source TEXT DEFAULT 'claude',
|
|
193
|
-
archived_at TEXT DEFAULT (datetime('now'))
|
|
194
|
-
)
|
|
195
|
-
""")
|
|
196
|
-
conn.execute("""
|
|
197
|
-
CREATE INDEX IF NOT EXISTS idx_diary_archive_created
|
|
198
|
-
ON diary_archive (created_at)
|
|
199
|
-
""")
|
|
200
|
-
conn.execute("""
|
|
201
|
-
CREATE INDEX IF NOT EXISTS idx_diary_archive_domain
|
|
202
|
-
ON diary_archive (domain)
|
|
203
|
-
""")
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def _m9_maintenance_schedule(conn):
|
|
207
|
-
conn.execute("""
|
|
208
|
-
CREATE TABLE IF NOT EXISTS maintenance_schedule (
|
|
209
|
-
task_name TEXT PRIMARY KEY,
|
|
210
|
-
interval_hours REAL NOT NULL,
|
|
211
|
-
last_run_at TEXT DEFAULT NULL,
|
|
212
|
-
last_duration_ms INTEGER DEFAULT 0,
|
|
213
|
-
run_count INTEGER DEFAULT 0
|
|
214
|
-
)
|
|
215
|
-
""")
|
|
216
|
-
tasks = [
|
|
217
|
-
('cognitive_decay', 20), ('synthesis', 20), ('self_audit', 144),
|
|
218
|
-
('weight_learning', 20), ('somatic_projection', 20), ('somatic_decay', 20),
|
|
219
|
-
('graph_maintenance', 48),
|
|
220
|
-
]
|
|
221
|
-
for name, hours in tasks:
|
|
222
|
-
conn.execute(
|
|
223
|
-
"INSERT OR IGNORE INTO maintenance_schedule (task_name, interval_hours) VALUES (?, ?)",
|
|
224
|
-
(name, hours)
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def _m12_session_checkpoints(conn):
|
|
229
|
-
"""Session checkpoints for intelligent auto-compaction.
|
|
230
|
-
|
|
231
|
-
PreCompact saves a checkpoint; PostCompact reads it to re-inject a
|
|
232
|
-
Core Memory Block that preserves continuity after context compression.
|
|
233
|
-
"""
|
|
234
|
-
conn.execute("""
|
|
235
|
-
CREATE TABLE IF NOT EXISTS session_checkpoints (
|
|
236
|
-
sid TEXT PRIMARY KEY,
|
|
237
|
-
task TEXT DEFAULT '',
|
|
238
|
-
task_status TEXT DEFAULT 'active',
|
|
239
|
-
active_files TEXT DEFAULT '[]',
|
|
240
|
-
current_goal TEXT DEFAULT '',
|
|
241
|
-
decisions_summary TEXT DEFAULT '',
|
|
242
|
-
errors_found TEXT DEFAULT '',
|
|
243
|
-
reasoning_thread TEXT DEFAULT '',
|
|
244
|
-
next_step TEXT DEFAULT '',
|
|
245
|
-
compaction_count INTEGER DEFAULT 0,
|
|
246
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
247
|
-
updated_at TEXT DEFAULT (datetime('now'))
|
|
248
|
-
)
|
|
249
|
-
""")
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def _m13_claude_session_id(conn):
|
|
253
|
-
"""Add claude_session_id to sessions for inter-terminal coordination (D+)."""
|
|
254
|
-
_migrate_add_column(conn, "sessions", "claude_session_id", "TEXT DEFAULT ''")
|
|
255
|
-
_migrate_add_index(conn, "idx_sessions_claude_sid", "sessions", "claude_session_id")
|
|
256
|
-
conn.commit()
|
|
257
|
-
|
|
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
|
-
|
|
298
|
-
# Migration registry — APPEND ONLY, never reorder or delete
|
|
299
|
-
MIGRATIONS = [
|
|
300
|
-
(1, "learnings_columns", _m1_learnings_columns),
|
|
301
|
-
(2, "followups_reasoning", _m2_followups_reasoning),
|
|
302
|
-
(3, "decisions_review", _m3_decisions_review),
|
|
303
|
-
(4, "session_diary_columns", _m4_session_diary_columns),
|
|
304
|
-
(5, "change_log_indexes", _m5_change_log_indexes),
|
|
305
|
-
(6, "error_guard_tables", _m6_error_guard_tables),
|
|
306
|
-
(7, "diary_source_and_draft", _m7_diary_source_and_draft),
|
|
307
|
-
(8, "adaptive_log_and_somatic", _m8_adaptive_log_and_somatic),
|
|
308
|
-
(9, "maintenance_schedule", _m9_maintenance_schedule),
|
|
309
|
-
(10, "diary_archive", _m10_diary_archive),
|
|
310
|
-
(11, "artifact_registry", _m11_artifact_registry),
|
|
311
|
-
(12, "session_checkpoints", _m12_session_checkpoints),
|
|
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),
|
|
315
|
-
]
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
def run_migrations(conn=None):
|
|
319
|
-
"""Run pending migrations. Tracks applied versions in schema_migrations.
|
|
320
|
-
|
|
321
|
-
Safe to call multiple times — skips already-applied migrations.
|
|
322
|
-
Called automatically by init_db() on every server start.
|
|
323
|
-
"""
|
|
324
|
-
if conn is None:
|
|
325
|
-
conn = get_db()
|
|
326
|
-
|
|
327
|
-
conn.execute("""
|
|
328
|
-
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
329
|
-
version INTEGER PRIMARY KEY,
|
|
330
|
-
name TEXT NOT NULL,
|
|
331
|
-
applied_at TEXT DEFAULT (datetime('now'))
|
|
332
|
-
)
|
|
333
|
-
""")
|
|
334
|
-
conn.commit()
|
|
335
|
-
|
|
336
|
-
applied = {r[0] for r in conn.execute("SELECT version FROM schema_migrations").fetchall()}
|
|
337
|
-
|
|
338
|
-
for version, name, fn in MIGRATIONS:
|
|
339
|
-
if version not in applied:
|
|
340
|
-
try:
|
|
341
|
-
fn(conn)
|
|
342
|
-
conn.execute(
|
|
343
|
-
"INSERT INTO schema_migrations (version, name) VALUES (?, ?)",
|
|
344
|
-
(version, name)
|
|
345
|
-
)
|
|
346
|
-
conn.commit()
|
|
347
|
-
except Exception as e:
|
|
348
|
-
# Log but don't crash — partial migration is better than no server
|
|
349
|
-
import sys
|
|
350
|
-
print(f"[MIGRATION] v{version} ({name}) failed: {e}", file=sys.stderr)
|
|
351
|
-
|
|
352
|
-
return len(MIGRATIONS) - len(applied)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
def get_schema_version() -> int:
|
|
356
|
-
"""Return the highest applied migration version, or 0 if none."""
|
|
357
|
-
conn = get_db()
|
|
358
|
-
try:
|
|
359
|
-
row = conn.execute("SELECT MAX(version) FROM schema_migrations").fetchone()
|
|
360
|
-
return row[0] or 0
|
|
361
|
-
except Exception:
|
|
362
|
-
return 0
|
|
363
|
-
|
|
364
|
-
|
package/src/db/_sessions 2.py
DELETED
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
"""NEXO DB — Sessions module."""
|
|
2
|
-
import time, secrets, string, sqlite3
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
from db._core import get_db, _gen_id, now_epoch, local_time_str, SESSION_STALE_SECONDS, MESSAGE_TTL_SECONDS, QUESTION_TTL_SECONDS
|
|
5
|
-
|
|
6
|
-
# ── Session operations ──────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
def now_epoch() -> float:
|
|
9
|
-
return time.time()
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def local_time_str() -> str:
|
|
13
|
-
from datetime import datetime
|
|
14
|
-
return datetime.now().strftime("%H:%M")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def register_session(sid: str, task: str, claude_session_id: str = "") -> dict:
|
|
18
|
-
"""Register or re-register a session."""
|
|
19
|
-
conn = get_db()
|
|
20
|
-
now = now_epoch()
|
|
21
|
-
conn.execute(
|
|
22
|
-
"INSERT OR REPLACE INTO sessions (sid, task, started_epoch, last_update_epoch, local_time, claude_session_id) "
|
|
23
|
-
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
24
|
-
(sid, task, now, now, local_time_str(), claude_session_id)
|
|
25
|
-
)
|
|
26
|
-
conn.commit()
|
|
27
|
-
return {"sid": sid, "task": task}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def update_session(sid: str, task: str | None) -> dict:
|
|
31
|
-
"""Update session timestamp (and task if provided). Preserves started_epoch.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
sid: Session ID.
|
|
35
|
-
task: New task description, or None to keep current task (keepalive touch).
|
|
36
|
-
"""
|
|
37
|
-
conn = get_db()
|
|
38
|
-
now = now_epoch()
|
|
39
|
-
row = conn.execute("SELECT started_epoch, task FROM sessions WHERE sid = ?", (sid,)).fetchone()
|
|
40
|
-
if row:
|
|
41
|
-
effective_task = task if task is not None else row["task"]
|
|
42
|
-
conn.execute(
|
|
43
|
-
"UPDATE sessions SET task = ?, last_update_epoch = ?, local_time = ? WHERE sid = ?",
|
|
44
|
-
(effective_task, now, local_time_str(), sid)
|
|
45
|
-
)
|
|
46
|
-
else:
|
|
47
|
-
effective_task = task or "Unknown"
|
|
48
|
-
conn.execute(
|
|
49
|
-
"INSERT INTO sessions (sid, task, started_epoch, last_update_epoch, local_time) "
|
|
50
|
-
"VALUES (?, ?, ?, ?, ?)",
|
|
51
|
-
(sid, effective_task, now, now, local_time_str())
|
|
52
|
-
)
|
|
53
|
-
conn.commit()
|
|
54
|
-
return {"sid": sid, "task": effective_task}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def complete_session(sid: str):
|
|
58
|
-
"""Remove session and its tracked files."""
|
|
59
|
-
conn = get_db()
|
|
60
|
-
conn.execute("PRAGMA foreign_keys=ON")
|
|
61
|
-
conn.execute("DELETE FROM tracked_files WHERE sid = ?", (sid,))
|
|
62
|
-
conn.execute("DELETE FROM sessions WHERE sid = ?", (sid,))
|
|
63
|
-
conn.commit()
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def get_active_sessions() -> list[dict]:
|
|
67
|
-
"""Get all sessions updated within STALE threshold."""
|
|
68
|
-
conn = get_db()
|
|
69
|
-
cutoff = now_epoch() - SESSION_STALE_SECONDS
|
|
70
|
-
rows = conn.execute(
|
|
71
|
-
"SELECT sid, task, started_epoch, last_update_epoch, local_time "
|
|
72
|
-
"FROM sessions WHERE last_update_epoch > ?",
|
|
73
|
-
(cutoff,)
|
|
74
|
-
).fetchall()
|
|
75
|
-
return [dict(r) for r in rows]
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def clean_stale_sessions() -> int:
|
|
79
|
-
"""Remove stale sessions. Returns count removed."""
|
|
80
|
-
conn = get_db()
|
|
81
|
-
cutoff = now_epoch() - SESSION_STALE_SECONDS
|
|
82
|
-
stale = conn.execute(
|
|
83
|
-
"SELECT sid FROM sessions WHERE last_update_epoch <= ?", (cutoff,)
|
|
84
|
-
).fetchall()
|
|
85
|
-
for row in stale:
|
|
86
|
-
conn.execute("DELETE FROM tracked_files WHERE sid = ?", (row["sid"],))
|
|
87
|
-
result = conn.execute(
|
|
88
|
-
"DELETE FROM sessions WHERE last_update_epoch <= ?", (cutoff,)
|
|
89
|
-
)
|
|
90
|
-
count = result.rowcount
|
|
91
|
-
conn.commit()
|
|
92
|
-
return count
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def search_sessions(keyword: str) -> list[dict]:
|
|
96
|
-
"""Find sessions whose task contains keyword (case-insensitive)."""
|
|
97
|
-
conn = get_db()
|
|
98
|
-
cutoff = now_epoch() - SESSION_STALE_SECONDS
|
|
99
|
-
rows = conn.execute(
|
|
100
|
-
"SELECT sid, task, last_update_epoch, local_time FROM sessions "
|
|
101
|
-
"WHERE last_update_epoch > ? AND LOWER(task) LIKE ?",
|
|
102
|
-
(cutoff, f"%{keyword.lower()}%")
|
|
103
|
-
).fetchall()
|
|
104
|
-
return [dict(r) for r in rows]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# ── File tracking ───────────────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
def track_files(sid: str, paths: list[str]) -> dict:
|
|
110
|
-
"""Track files for a session. Returns conflicts if any."""
|
|
111
|
-
conn = get_db()
|
|
112
|
-
now = now_epoch()
|
|
113
|
-
session = conn.execute("SELECT sid FROM sessions WHERE sid = ?", (sid,)).fetchone()
|
|
114
|
-
if not session:
|
|
115
|
-
return {"error": f"Session {sid} not found. Register first."}
|
|
116
|
-
|
|
117
|
-
for path in paths:
|
|
118
|
-
conn.execute(
|
|
119
|
-
"INSERT OR IGNORE INTO tracked_files (sid, path, tracked_at) VALUES (?, ?, ?)",
|
|
120
|
-
(sid, path, now)
|
|
121
|
-
)
|
|
122
|
-
conn.commit()
|
|
123
|
-
conflicts = _check_conflicts(conn, sid)
|
|
124
|
-
return {"tracked": paths, "conflicts": conflicts}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def untrack_files(sid: str, paths: list[str] | None = None):
|
|
128
|
-
"""Untrack files. If paths is None, untrack all."""
|
|
129
|
-
conn = get_db()
|
|
130
|
-
if paths:
|
|
131
|
-
for path in paths:
|
|
132
|
-
conn.execute(
|
|
133
|
-
"DELETE FROM tracked_files WHERE sid = ? AND path = ?",
|
|
134
|
-
(sid, path)
|
|
135
|
-
)
|
|
136
|
-
else:
|
|
137
|
-
conn.execute("DELETE FROM tracked_files WHERE sid = ?", (sid,))
|
|
138
|
-
conn.commit()
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def get_all_tracked_files() -> dict:
|
|
142
|
-
"""Get all tracked files grouped by session."""
|
|
143
|
-
conn = get_db()
|
|
144
|
-
cutoff = now_epoch() - SESSION_STALE_SECONDS
|
|
145
|
-
rows = conn.execute(
|
|
146
|
-
"SELECT tf.sid, tf.path, s.task FROM tracked_files tf "
|
|
147
|
-
"JOIN sessions s ON tf.sid = s.sid "
|
|
148
|
-
"WHERE s.last_update_epoch > ?",
|
|
149
|
-
(cutoff,)
|
|
150
|
-
).fetchall()
|
|
151
|
-
result = {}
|
|
152
|
-
for r in rows:
|
|
153
|
-
sid = r["sid"]
|
|
154
|
-
if sid not in result:
|
|
155
|
-
result[sid] = {"task": r["task"], "files": []}
|
|
156
|
-
result[sid]["files"].append(r["path"])
|
|
157
|
-
return result
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def _check_conflicts(conn: sqlite3.Connection, sid: str) -> list[dict]:
|
|
161
|
-
"""Check if any of sid's files are tracked by other active sessions."""
|
|
162
|
-
cutoff = now_epoch() - SESSION_STALE_SECONDS
|
|
163
|
-
my_files = conn.execute(
|
|
164
|
-
"SELECT path FROM tracked_files WHERE sid = ?", (sid,)
|
|
165
|
-
).fetchall()
|
|
166
|
-
my_paths = {r["path"] for r in my_files}
|
|
167
|
-
if not my_paths:
|
|
168
|
-
return []
|
|
169
|
-
|
|
170
|
-
conflicts = []
|
|
171
|
-
others = conn.execute(
|
|
172
|
-
"SELECT tf.sid, tf.path, s.task FROM tracked_files tf "
|
|
173
|
-
"JOIN sessions s ON tf.sid = s.sid "
|
|
174
|
-
"WHERE tf.sid != ? AND s.last_update_epoch > ?",
|
|
175
|
-
(sid, cutoff)
|
|
176
|
-
).fetchall()
|
|
177
|
-
by_sid = {}
|
|
178
|
-
for r in others:
|
|
179
|
-
if r["path"] in my_paths:
|
|
180
|
-
osid = r["sid"]
|
|
181
|
-
if osid not in by_sid:
|
|
182
|
-
by_sid[osid] = {"sid": osid, "task": r["task"], "files": []}
|
|
183
|
-
by_sid[osid]["files"].append(r["path"])
|
|
184
|
-
return list(by_sid.values())
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
# ── Messages ────────────────────────────────────────────────────────
|
|
188
|
-
|
|
189
|
-
def send_message(from_sid: str, to_sid: str, text: str) -> str:
|
|
190
|
-
"""Send a message. to_sid can be 'all' for broadcast."""
|
|
191
|
-
conn = get_db()
|
|
192
|
-
_clean_old_messages(conn)
|
|
193
|
-
msg_id = _gen_id("msg", 6)
|
|
194
|
-
conn.execute(
|
|
195
|
-
"INSERT INTO messages (id, from_sid, to_sid, text, created_epoch) "
|
|
196
|
-
"VALUES (?, ?, ?, ?, ?)",
|
|
197
|
-
(msg_id, from_sid, to_sid, text, now_epoch())
|
|
198
|
-
)
|
|
199
|
-
conn.commit()
|
|
200
|
-
return msg_id
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def get_inbox(sid: str) -> list[dict]:
|
|
204
|
-
"""Get unread messages for a session."""
|
|
205
|
-
conn = get_db()
|
|
206
|
-
_clean_old_messages(conn)
|
|
207
|
-
rows = conn.execute(
|
|
208
|
-
"SELECT m.id, m.from_sid, m.to_sid, m.text, m.created_epoch "
|
|
209
|
-
"FROM messages m "
|
|
210
|
-
"WHERE (m.to_sid = 'all' OR m.to_sid = ?) "
|
|
211
|
-
"AND m.from_sid != ? "
|
|
212
|
-
"AND m.id NOT IN (SELECT message_id FROM message_reads WHERE sid = ?)",
|
|
213
|
-
(sid, sid, sid)
|
|
214
|
-
).fetchall()
|
|
215
|
-
for r in rows:
|
|
216
|
-
conn.execute(
|
|
217
|
-
"INSERT OR IGNORE INTO message_reads (message_id, sid) VALUES (?, ?)",
|
|
218
|
-
(r["id"], sid)
|
|
219
|
-
)
|
|
220
|
-
conn.commit()
|
|
221
|
-
result = [dict(r) for r in rows]
|
|
222
|
-
return result
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def _clean_old_messages(conn: sqlite3.Connection):
|
|
226
|
-
"""Remove expired messages and commit immediately."""
|
|
227
|
-
cutoff = now_epoch() - MESSAGE_TTL_SECONDS
|
|
228
|
-
conn.execute("DELETE FROM messages WHERE created_epoch < ?", (cutoff,))
|
|
229
|
-
conn.commit()
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
# ── Questions ───────────────────────────────────────────────────────
|
|
233
|
-
|
|
234
|
-
def ask_question(from_sid: str, to_sid: str, question: str) -> str:
|
|
235
|
-
"""Create a pending question. Returns qid."""
|
|
236
|
-
conn = get_db()
|
|
237
|
-
_expire_old_questions(conn)
|
|
238
|
-
qid = _gen_id("q", 8)
|
|
239
|
-
conn.execute(
|
|
240
|
-
"INSERT INTO questions (qid, from_sid, to_sid, question, status, created_epoch) "
|
|
241
|
-
"VALUES (?, ?, ?, ?, 'pending', ?)",
|
|
242
|
-
(qid, from_sid, to_sid, question, now_epoch())
|
|
243
|
-
)
|
|
244
|
-
conn.commit()
|
|
245
|
-
return qid
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def answer_question(qid: str, answer: str) -> dict:
|
|
249
|
-
"""Answer a pending question."""
|
|
250
|
-
conn = get_db()
|
|
251
|
-
row = conn.execute(
|
|
252
|
-
"SELECT * FROM questions WHERE qid = ?", (qid,)
|
|
253
|
-
).fetchone()
|
|
254
|
-
if not row:
|
|
255
|
-
return {"error": f"Question {qid} not found"}
|
|
256
|
-
if row["status"] != "pending":
|
|
257
|
-
return {"error": f"Question {qid} is {row['status']}, not pending"}
|
|
258
|
-
conn.execute(
|
|
259
|
-
"UPDATE questions SET answer = ?, status = 'answered', answered_epoch = ? "
|
|
260
|
-
"WHERE qid = ?",
|
|
261
|
-
(answer, now_epoch(), qid)
|
|
262
|
-
)
|
|
263
|
-
conn.commit()
|
|
264
|
-
return {"qid": qid, "status": "answered"}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def get_pending_questions(sid: str) -> list[dict]:
|
|
268
|
-
"""Get pending questions addressed to this session."""
|
|
269
|
-
conn = get_db()
|
|
270
|
-
_expire_old_questions(conn)
|
|
271
|
-
rows = conn.execute(
|
|
272
|
-
"SELECT qid, from_sid, question, created_epoch FROM questions "
|
|
273
|
-
"WHERE to_sid = ? AND status = 'pending'",
|
|
274
|
-
(sid,)
|
|
275
|
-
).fetchall()
|
|
276
|
-
conn.commit()
|
|
277
|
-
return [dict(r) for r in rows]
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
def check_answer(qid: str) -> dict | None:
|
|
281
|
-
"""Check if a question has been answered. Returns answer or None."""
|
|
282
|
-
conn = get_db()
|
|
283
|
-
row = conn.execute(
|
|
284
|
-
"SELECT qid, answer, status FROM questions WHERE qid = ?", (qid,)
|
|
285
|
-
).fetchone()
|
|
286
|
-
if not row:
|
|
287
|
-
return None
|
|
288
|
-
return dict(row)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def _expire_old_questions(conn: sqlite3.Connection):
|
|
292
|
-
"""Mark old pending questions as expired."""
|
|
293
|
-
cutoff = now_epoch() - QUESTION_TTL_SECONDS
|
|
294
|
-
conn.execute(
|
|
295
|
-
"UPDATE questions SET status = 'expired' "
|
|
296
|
-
"WHERE status = 'pending' AND created_epoch < ?",
|
|
297
|
-
(cutoff,)
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
|