nexo-brain 1.7.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -60
- package/bin/nexo-brain.js +680 -381
- package/package.json +18 -3
- package/scripts/migrate-to-unified.sh +813 -0
- package/scripts/migrate-v1.7-to-v1.8.py +214 -0
- package/scripts/pre-commit-check.sh +1 -1
- package/src/__pycache__/auto_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__/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.py +1 -1
- package/src/auto_update.py +634 -0
- 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.py +7 -3
- package/src/cognitive/_decay.py +1 -1
- package/src/cognitive/_search.py +1 -0
- package/src/cognitive/_trust.py +3 -3
- package/src/crons/manifest.json +106 -0
- package/src/crons/sync.py +217 -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.py +24 -4
- package/src/dashboard/templates/dashboard.html +3 -2
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core.py +5 -1
- package/src/db/_episodic.py +2 -4
- package/src/db/_reminders.py +45 -6
- package/src/db/_schema.py +31 -0
- package/src/evolution_cycle.py +33 -11
- package/src/hooks/auto_capture.py +1 -1
- package/src/hooks/capture-tool-logs.sh +76 -0
- package/src/hooks/inbox-hook.sh +2 -1
- package/src/hooks/post-compact.sh +2 -1
- package/src/hooks/pre-compact.sh +104 -2
- package/src/hooks/session-start.sh +6 -2
- package/src/hooks/session-stop.sh +4 -2
- package/src/kg_populate.py +4 -1
- package/src/migrate_embeddings.py +4 -1
- package/src/plugin_loader.py +100 -34
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/agents.py +2 -2
- package/src/plugins/backup.py +5 -4
- package/src/plugins/core_rules.py +37 -16
- package/src/plugins/episodic_memory.py +14 -5
- package/src/plugins/evolution.py +6 -2
- package/src/plugins/guard.py +20 -11
- package/src/plugins/update.py +256 -0
- package/src/requirements.txt +12 -0
- package/src/scripts/check-context.py +8 -3
- package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/apply_findings.py +514 -169
- package/src/scripts/deep-sleep/collect.py +480 -0
- package/src/scripts/deep-sleep/extract-prompt.md +233 -0
- package/src/scripts/deep-sleep/extract.py +249 -0
- package/src/scripts/deep-sleep/synthesize-prompt.md +168 -0
- package/src/scripts/deep-sleep/synthesize.py +191 -0
- package/src/scripts/nexo-auto-update.py +4 -211
- package/src/scripts/nexo-backup.sh +5 -13
- package/src/scripts/nexo-brain-activation.sh +26 -26
- package/src/scripts/nexo-catchup.py +36 -25
- package/src/scripts/nexo-cognitive-decay.py +7 -3
- package/src/scripts/nexo-daily-self-audit.py +44 -15
- package/src/scripts/nexo-deep-sleep.sh +31 -16
- package/src/scripts/nexo-evolution-run.py +21 -11
- package/src/scripts/nexo-followup-hygiene.py +6 -4
- package/src/scripts/nexo-github-monitor.py +12 -8
- package/src/scripts/nexo-immune.py +6 -4
- package/src/scripts/nexo-inbox-hook.sh +2 -1
- package/src/scripts/nexo-install.py +4 -225
- package/src/scripts/nexo-learning-housekeep.py +7 -3
- package/src/scripts/nexo-learning-validator.py +1 -22
- package/src/scripts/nexo-migrate.py +9 -3
- package/src/scripts/nexo-postmortem-consolidator.py +17 -10
- package/src/scripts/nexo-pre-commit.py +3 -1
- package/src/scripts/nexo-prevent-sleep.sh +29 -0
- package/src/scripts/nexo-proactive-dashboard.py +5 -4
- package/src/scripts/nexo-runtime-preflight.py +59 -55
- package/src/scripts/nexo-send-email.py +1 -1
- package/src/scripts/nexo-send-reply.py +3 -1
- package/src/scripts/nexo-sleep.py +13 -9
- package/src/scripts/nexo-snapshot-restore.sh +2 -1
- package/src/scripts/nexo-synthesis.py +11 -7
- package/src/scripts/nexo-tcc-approve.sh +79 -0
- package/src/scripts/nexo-update.sh +161 -0
- package/src/scripts/nexo-watchdog-smoke.py +18 -13
- package/src/scripts/nexo-watchdog.sh +22 -13
- package/src/server.py +77 -28
- package/src/storage_router.py +6 -2
- package/src/tools_learnings.py +6 -6
- package/src/tools_menu.py +1 -1
- package/src/tools_reminders_crud.py +10 -8
- package/src/tools_sessions.py +76 -4
- package/templates/CLAUDE.md.template +14 -80
- package/templates/launchagents/README.md +7 -7
- package/templates/launchagents/com.nexo.auto-close-sessions.plist +5 -1
- package/templates/launchagents/com.nexo.catchup.plist +4 -0
- package/templates/launchagents/com.nexo.cognitive-decay.plist +7 -0
- package/templates/launchagents/com.nexo.dashboard.plist +5 -1
- package/templates/launchagents/com.nexo.deep-sleep.plist +4 -0
- package/templates/launchagents/com.nexo.evolution.plist +4 -0
- package/templates/launchagents/com.nexo.followup-hygiene.plist +4 -0
- package/templates/launchagents/com.nexo.github-monitor.plist +3 -1
- package/templates/launchagents/com.nexo.immune.plist +4 -0
- package/templates/launchagents/com.nexo.postmortem.plist +4 -0
- package/templates/launchagents/com.nexo.self-audit.plist +4 -0
- package/templates/launchagents/com.nexo.synthesis.plist +4 -0
- package/templates/launchagents/com.nexo.watchdog.plist +4 -0
- package/templates/openclaw.json +1 -1
- package/tests/conftest.py +2 -2
- package/tests/test_cognitive.py +7 -6
- package/tests/test_migrations.py +26 -0
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/claim_graph.cpython-314.pyc +0 -0
- package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
- package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
- package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/server.cpython-314.pyc +0 -0
- package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
- package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
- package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
- 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-github-monitor.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/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/analyze_session.py +0 -217
- package/src/scripts/deep-sleep/collect_transcripts.py +0 -145
- package/src/scripts/deep-sleep/prompt.md +0 -109
- package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_migrations.cpython-314-pytest-9.0.2.pyc +0 -0
package/src/cognitive/_core.py
CHANGED
|
@@ -11,10 +11,14 @@ from datetime import datetime, timedelta
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Optional
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
|
|
15
|
+
_data_dir = os.path.join(NEXO_HOME, "data")
|
|
16
|
+
os.makedirs(_data_dir, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
COGNITIVE_DB = os.path.join(_data_dir, "cognitive.db")
|
|
15
19
|
EMBEDDING_DIM = 768
|
|
16
|
-
LAMBDA_STM = 0.
|
|
17
|
-
LAMBDA_LTM = 0.
|
|
20
|
+
LAMBDA_STM = 0.004126 # half-life = ln(2) / (7 * 24) ≈ 7 days
|
|
21
|
+
LAMBDA_LTM = 0.000481 # half-life = ln(2) / (60 * 24) ≈ 60 days
|
|
18
22
|
|
|
19
23
|
# Prediction Error Gate thresholds
|
|
20
24
|
PE_GATE_REJECT = 0.85 # similarity > this → reject (not novel enough)
|
package/src/cognitive/_decay.py
CHANGED
|
@@ -254,7 +254,7 @@ def dream_cycle(max_insights: int = 50) -> dict:
|
|
|
254
254
|
})
|
|
255
255
|
|
|
256
256
|
if len(recent_memories) < 2:
|
|
257
|
-
return {"insights_created": 0, "insights": [], "memories_scanned": len(recent_memories)}
|
|
257
|
+
return {"insights_created": 0, "insights": [], "memories_scanned": len(recent_memories), "candidates_found": 0}
|
|
258
258
|
|
|
259
259
|
# 2. Get already-dreamed pairs to skip
|
|
260
260
|
dreamed = set()
|
package/src/cognitive/_search.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""NEXO Cognitive — Search, retrieval, ranking."""
|
|
2
2
|
import math
|
|
3
|
+
import sqlite3
|
|
3
4
|
import numpy as np
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from cognitive._core import _get_db, embed, cosine_similarity, _blob_to_array, _array_to_blob, _get_model, _get_reranker, rerank_results, EMBEDDING_DIM
|
package/src/cognitive/_trust.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""NEXO Cognitive — Trust scoring, sentiment, dissonance."""
|
|
2
2
|
import re
|
|
3
3
|
import numpy as np
|
|
4
|
-
from datetime import datetime
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
5
|
from cognitive._core import _get_db, embed, cosine_similarity, _blob_to_array
|
|
6
6
|
from cognitive._core import POSITIVE_SIGNALS, NEGATIVE_SIGNALS, URGENCY_SIGNALS
|
|
7
7
|
|
|
@@ -314,13 +314,13 @@ def detect_sentiment(text: str) -> dict:
|
|
|
314
314
|
sentiment = "negative"
|
|
315
315
|
intensity = min(1.0, 0.3 + neg_score * 0.15)
|
|
316
316
|
if intensity > 0.7:
|
|
317
|
-
guidance = "MODE: Ultra-
|
|
317
|
+
guidance = "MODE: Ultra-concise. Zero explanations. Solve and show result."
|
|
318
318
|
else:
|
|
319
319
|
guidance = "MODE: Concise. Less context, more direct action."
|
|
320
320
|
elif pos_score > neg_score and pos_score >= 1:
|
|
321
321
|
sentiment = "positive"
|
|
322
322
|
intensity = min(1.0, 0.3 + pos_score * 0.15)
|
|
323
|
-
guidance = "MODE: Normal.
|
|
323
|
+
guidance = "MODE: Normal. Good time to suggest backlog ideas or improvements."
|
|
324
324
|
elif urgency_hits:
|
|
325
325
|
sentiment = "urgent"
|
|
326
326
|
intensity = 0.8
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "NEXO cron manifest — synced by nexo_update to LaunchAgents (macOS) or systemd timers (Linux)",
|
|
3
|
+
"version": 2,
|
|
4
|
+
"crons": [
|
|
5
|
+
{
|
|
6
|
+
"id": "deep-sleep",
|
|
7
|
+
"script": "scripts/nexo-deep-sleep.sh",
|
|
8
|
+
"type": "shell",
|
|
9
|
+
"schedule": {"hour": 4, "minute": 30},
|
|
10
|
+
"description": "Overnight session analysis — 4 phases: collect, extract, synthesize, apply",
|
|
11
|
+
"core": true
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "sleep",
|
|
15
|
+
"script": "scripts/nexo-sleep.py",
|
|
16
|
+
"schedule": {"hour": 4, "minute": 0},
|
|
17
|
+
"description": "Nightly memory consolidation and dream cycle",
|
|
18
|
+
"core": true
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "cognitive-decay",
|
|
22
|
+
"script": "scripts/nexo-cognitive-decay.py",
|
|
23
|
+
"schedule": {"hour": 3, "minute": 0},
|
|
24
|
+
"description": "Memory decay — reduce strength of unaccessed memories",
|
|
25
|
+
"core": true
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "learning-housekeep",
|
|
29
|
+
"script": "scripts/nexo-learning-housekeep.py",
|
|
30
|
+
"schedule": {"hour": 3, "minute": 15},
|
|
31
|
+
"description": "Archive stale learnings, deduplicate, validate",
|
|
32
|
+
"core": true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "immune",
|
|
36
|
+
"script": "scripts/nexo-immune.py",
|
|
37
|
+
"interval_seconds": 1800,
|
|
38
|
+
"description": "Health monitor — checks MCP, DB, services, auto-repairs",
|
|
39
|
+
"core": true
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "watchdog",
|
|
43
|
+
"script": "scripts/nexo-watchdog.sh",
|
|
44
|
+
"type": "shell",
|
|
45
|
+
"interval_seconds": 1800,
|
|
46
|
+
"description": "System health checks — snapshots, logs, alerts",
|
|
47
|
+
"core": true
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "self-audit",
|
|
51
|
+
"script": "scripts/nexo-daily-self-audit.py",
|
|
52
|
+
"schedule": {"hour": 7, "minute": 0},
|
|
53
|
+
"description": "Daily self-audit — validates learnings, protocols, drift",
|
|
54
|
+
"core": true
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "postmortem",
|
|
58
|
+
"script": "scripts/nexo-postmortem-consolidator.py",
|
|
59
|
+
"schedule": {"hour": 23, "minute": 30},
|
|
60
|
+
"description": "Consolidate session post-mortems into patterns",
|
|
61
|
+
"core": true
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "evolution",
|
|
65
|
+
"script": "scripts/nexo-evolution-run.py",
|
|
66
|
+
"schedule": {"hour": 5, "minute": 0, "weekday": 0},
|
|
67
|
+
"description": "Weekly self-improvement cycle — propose and evaluate changes",
|
|
68
|
+
"core": true
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "followup-hygiene",
|
|
72
|
+
"script": "scripts/nexo-followup-hygiene.py",
|
|
73
|
+
"schedule": {"hour": 5, "minute": 0},
|
|
74
|
+
"description": "Clean stale followups, archive completed, validate dates",
|
|
75
|
+
"core": true
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "synthesis",
|
|
79
|
+
"script": "scripts/nexo-synthesis.py",
|
|
80
|
+
"interval_seconds": 7200,
|
|
81
|
+
"description": "Periodic synthesis — cross-reference learnings, decisions, changes",
|
|
82
|
+
"core": true
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "auto-close-sessions",
|
|
86
|
+
"script": "scripts/nexo-auto-close-sessions.py",
|
|
87
|
+
"interval_seconds": 300,
|
|
88
|
+
"description": "Close stale sessions that lost their parent process",
|
|
89
|
+
"core": true
|
|
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
|
+
{
|
|
99
|
+
"id": "catchup",
|
|
100
|
+
"script": "scripts/nexo-catchup.py",
|
|
101
|
+
"schedule": {"hour": 8, "minute": 30},
|
|
102
|
+
"description": "Morning catchup briefing for the user",
|
|
103
|
+
"core": true
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
NEXO Cron Sync — Synchronize crons/manifest.json with system LaunchAgents (macOS).
|
|
4
|
+
|
|
5
|
+
Called by nexo_update after pulling new code. Ensures:
|
|
6
|
+
- New crons in manifest → installed
|
|
7
|
+
- Removed crons from manifest → unloaded + deleted
|
|
8
|
+
- Changed schedule/interval → plist updated + reloaded
|
|
9
|
+
- Personal (non-core) crons → left untouched
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
python3 crons/sync.py [--dry-run]
|
|
13
|
+
|
|
14
|
+
Environment:
|
|
15
|
+
NEXO_HOME — root of NEXO installation
|
|
16
|
+
NEXO_CODE — path to NEXO source (defaults to script parent's parent)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import platform
|
|
22
|
+
import plistlib
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
28
|
+
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent.parent)))
|
|
29
|
+
MANIFEST = Path(__file__).resolve().parent / "manifest.json"
|
|
30
|
+
LAUNCH_AGENTS_DIR = Path.home() / "Library" / "LaunchAgents"
|
|
31
|
+
LABEL_PREFIX = "com.nexo."
|
|
32
|
+
LOG_DIR = NEXO_HOME / "logs"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def log(msg: str):
|
|
36
|
+
print(f"[cron-sync] {msg}", flush=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_manifest() -> list[dict]:
|
|
40
|
+
with open(MANIFEST) as f:
|
|
41
|
+
data = json.load(f)
|
|
42
|
+
return data.get("crons", [])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_plist(cron: dict) -> dict:
|
|
46
|
+
"""Build a macOS LaunchAgent plist dict from a manifest entry."""
|
|
47
|
+
cron_id = cron["id"]
|
|
48
|
+
label = f"{LABEL_PREFIX}{cron_id}"
|
|
49
|
+
script_path = str(NEXO_CODE / cron["script"])
|
|
50
|
+
script_type = cron.get("type", "python")
|
|
51
|
+
|
|
52
|
+
if script_type == "shell":
|
|
53
|
+
program_args = ["/bin/bash", script_path]
|
|
54
|
+
else:
|
|
55
|
+
# Find python3
|
|
56
|
+
python_candidates = [
|
|
57
|
+
"/opt/homebrew/bin/python3",
|
|
58
|
+
"/usr/local/bin/python3",
|
|
59
|
+
"/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
|
|
60
|
+
"/usr/bin/python3",
|
|
61
|
+
]
|
|
62
|
+
python_bin = "python3"
|
|
63
|
+
for p in python_candidates:
|
|
64
|
+
if Path(p).exists():
|
|
65
|
+
python_bin = p
|
|
66
|
+
break
|
|
67
|
+
program_args = [python_bin, script_path]
|
|
68
|
+
|
|
69
|
+
plist = {
|
|
70
|
+
"Label": label,
|
|
71
|
+
"ProgramArguments": program_args,
|
|
72
|
+
"StandardOutPath": str(LOG_DIR / f"{cron_id}-stdout.log"),
|
|
73
|
+
"StandardErrorPath": str(LOG_DIR / f"{cron_id}-stderr.log"),
|
|
74
|
+
"EnvironmentVariables": {
|
|
75
|
+
"PATH": "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:"
|
|
76
|
+
+ str(Path.home() / ".local" / "bin") + ":"
|
|
77
|
+
+ str(Path.home() / ".nvm/versions/node/v22.14.0/bin") + ":"
|
|
78
|
+
+ "/Library/Frameworks/Python.framework/Versions/3.12/bin",
|
|
79
|
+
"HOME": str(Path.home()),
|
|
80
|
+
"NEXO_HOME": str(NEXO_HOME),
|
|
81
|
+
"NEXO_CODE": str(NEXO_CODE),
|
|
82
|
+
"PYTHONUNBUFFERED": "1",
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Schedule
|
|
87
|
+
if "interval_seconds" in cron:
|
|
88
|
+
plist["StartInterval"] = cron["interval_seconds"]
|
|
89
|
+
elif "schedule" in cron:
|
|
90
|
+
cal = {}
|
|
91
|
+
s = cron["schedule"]
|
|
92
|
+
if "hour" in s:
|
|
93
|
+
cal["Hour"] = s["hour"]
|
|
94
|
+
if "minute" in s:
|
|
95
|
+
cal["Minute"] = s["minute"]
|
|
96
|
+
if "weekday" in s:
|
|
97
|
+
cal["Weekday"] = s["weekday"]
|
|
98
|
+
plist["StartCalendarInterval"] = cal
|
|
99
|
+
|
|
100
|
+
return plist
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_installed_nexo_crons() -> dict[str, Path]:
|
|
104
|
+
"""Return dict of cron_id → plist_path for installed NEXO crons."""
|
|
105
|
+
installed = {}
|
|
106
|
+
if not LAUNCH_AGENTS_DIR.exists():
|
|
107
|
+
return installed
|
|
108
|
+
for f in LAUNCH_AGENTS_DIR.glob(f"{LABEL_PREFIX}*.plist"):
|
|
109
|
+
cron_id = f.stem.replace(LABEL_PREFIX, "")
|
|
110
|
+
installed[cron_id] = f
|
|
111
|
+
return installed
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def plist_needs_update(existing_path: Path, new_plist: dict) -> bool:
|
|
115
|
+
"""Check if the installed plist differs from what we'd generate."""
|
|
116
|
+
try:
|
|
117
|
+
with open(existing_path, "rb") as f:
|
|
118
|
+
existing = plistlib.load(f)
|
|
119
|
+
except Exception:
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
# Compare key fields
|
|
123
|
+
if existing.get("ProgramArguments") != new_plist.get("ProgramArguments"):
|
|
124
|
+
return True
|
|
125
|
+
if existing.get("StartInterval") != new_plist.get("StartInterval"):
|
|
126
|
+
return True
|
|
127
|
+
if existing.get("StartCalendarInterval") != new_plist.get("StartCalendarInterval"):
|
|
128
|
+
return True
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def install_plist(label: str, plist: dict, plist_path: Path, dry_run: bool):
|
|
133
|
+
"""Write plist and load it."""
|
|
134
|
+
if dry_run:
|
|
135
|
+
log(f" DRY-RUN: would install {plist_path.name}")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Unload if already loaded
|
|
139
|
+
subprocess.run(["launchctl", "unload", str(plist_path)], capture_output=True)
|
|
140
|
+
|
|
141
|
+
with open(plist_path, "wb") as f:
|
|
142
|
+
plistlib.dump(plist, f)
|
|
143
|
+
|
|
144
|
+
subprocess.run(["launchctl", "load", str(plist_path)], capture_output=True)
|
|
145
|
+
log(f" Installed + loaded: {plist_path.name}")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def unload_plist(plist_path: Path, dry_run: bool):
|
|
149
|
+
"""Unload and remove a plist."""
|
|
150
|
+
if dry_run:
|
|
151
|
+
log(f" DRY-RUN: would remove {plist_path.name}")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
subprocess.run(["launchctl", "unload", str(plist_path)], capture_output=True)
|
|
155
|
+
plist_path.unlink(missing_ok=True)
|
|
156
|
+
log(f" Removed: {plist_path.name}")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def sync(dry_run: bool = False):
|
|
160
|
+
if platform.system() != "Darwin":
|
|
161
|
+
log("Not macOS — cron sync only supports LaunchAgents. Skipping.")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
LAUNCH_AGENTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
|
|
167
|
+
manifest_crons = load_manifest()
|
|
168
|
+
manifest_ids = {c["id"] for c in manifest_crons}
|
|
169
|
+
installed = get_installed_nexo_crons()
|
|
170
|
+
|
|
171
|
+
log(f"Manifest: {len(manifest_crons)} core crons")
|
|
172
|
+
log(f"Installed: {len(installed)} NEXO crons")
|
|
173
|
+
|
|
174
|
+
# 1. Install or update crons from manifest
|
|
175
|
+
for cron in manifest_crons:
|
|
176
|
+
cron_id = cron["id"]
|
|
177
|
+
label = f"{LABEL_PREFIX}{cron_id}"
|
|
178
|
+
plist_path = LAUNCH_AGENTS_DIR / f"{label}.plist"
|
|
179
|
+
new_plist = build_plist(cron)
|
|
180
|
+
|
|
181
|
+
if cron_id not in installed:
|
|
182
|
+
log(f" NEW: {cron_id}")
|
|
183
|
+
install_plist(label, new_plist, plist_path, dry_run)
|
|
184
|
+
elif plist_needs_update(installed[cron_id], new_plist):
|
|
185
|
+
log(f" UPDATE: {cron_id}")
|
|
186
|
+
install_plist(label, new_plist, plist_path, dry_run)
|
|
187
|
+
else:
|
|
188
|
+
log(f" OK: {cron_id}")
|
|
189
|
+
|
|
190
|
+
# 2. Remove crons that are in installed but NOT in manifest and ARE core
|
|
191
|
+
# (personal crons like shopify-backup, email-monitor are left alone)
|
|
192
|
+
for cron_id, plist_path in installed.items():
|
|
193
|
+
if cron_id not in manifest_ids:
|
|
194
|
+
# Check if this was previously a core cron by reading the plist
|
|
195
|
+
# If it points to NEXO_CODE scripts → it's core, safe to remove
|
|
196
|
+
try:
|
|
197
|
+
with open(plist_path, "rb") as f:
|
|
198
|
+
existing = plistlib.load(f)
|
|
199
|
+
args = existing.get("ProgramArguments", [])
|
|
200
|
+
is_core = any(str(NEXO_CODE) in str(a) for a in args)
|
|
201
|
+
except Exception:
|
|
202
|
+
is_core = False
|
|
203
|
+
|
|
204
|
+
if is_core:
|
|
205
|
+
log(f" REMOVE (no longer in manifest): {cron_id}")
|
|
206
|
+
unload_plist(plist_path, dry_run)
|
|
207
|
+
else:
|
|
208
|
+
log(f" SKIP (personal): {cron_id}")
|
|
209
|
+
|
|
210
|
+
log("Sync complete.")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
dry_run = "--dry-run" in sys.argv
|
|
215
|
+
if dry_run:
|
|
216
|
+
log("DRY RUN MODE — no changes will be made")
|
|
217
|
+
sync(dry_run=dry_run)
|
|
Binary file
|
|
Binary file
|
package/src/dashboard/app.py
CHANGED
|
@@ -10,6 +10,7 @@ Usage:
|
|
|
10
10
|
import argparse
|
|
11
11
|
import json
|
|
12
12
|
import os
|
|
13
|
+
import platform
|
|
13
14
|
import subprocess
|
|
14
15
|
import sys
|
|
15
16
|
import time
|
|
@@ -22,7 +23,7 @@ from fastapi.responses import HTMLResponse, JSONResponse
|
|
|
22
23
|
from fastapi.staticfiles import StaticFiles
|
|
23
24
|
from pydantic import BaseModel
|
|
24
25
|
|
|
25
|
-
# Add parent dir to path so we can import
|
|
26
|
+
# Add parent dir to path so we can import NEXO modules
|
|
26
27
|
_PARENT = str(Path(__file__).resolve().parent.parent)
|
|
27
28
|
if _PARENT not in sys.path:
|
|
28
29
|
sys.path.insert(0, _PARENT)
|
|
@@ -63,7 +64,7 @@ async def create_tables():
|
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
# ---------------------------------------------------------------------------
|
|
66
|
-
# Lazy imports — modules live in the parent
|
|
67
|
+
# Lazy imports — modules live in the parent source directory
|
|
67
68
|
# ---------------------------------------------------------------------------
|
|
68
69
|
|
|
69
70
|
def _cognitive():
|
|
@@ -307,15 +308,29 @@ async def api_adaptive():
|
|
|
307
308
|
|
|
308
309
|
@app.get("/api/sessions")
|
|
309
310
|
async def api_sessions(limit: int = Query(10, ge=1, le=50)):
|
|
310
|
-
"""Recent session diaries."""
|
|
311
|
+
"""Recent session diaries + active sessions from sessions table."""
|
|
311
312
|
db = _db()
|
|
312
313
|
conn = db.get_db()
|
|
314
|
+
# Active sessions (from sessions table, not diaries)
|
|
315
|
+
active_rows = conn.execute(
|
|
316
|
+
"SELECT sid as session_id, task, last_update_epoch, claude_session_id "
|
|
317
|
+
"FROM sessions WHERE last_update_epoch > (strftime('%s','now') - 900) "
|
|
318
|
+
"ORDER BY last_update_epoch DESC"
|
|
319
|
+
).fetchall()
|
|
320
|
+
active = [dict(r) for r in active_rows]
|
|
321
|
+
# Add last_heartbeat as ISO string for frontend
|
|
322
|
+
for a in active:
|
|
323
|
+
epoch = a.get("last_update_epoch", 0)
|
|
324
|
+
if epoch:
|
|
325
|
+
import datetime
|
|
326
|
+
a["last_heartbeat"] = datetime.datetime.fromtimestamp(epoch).isoformat()
|
|
327
|
+
# Recent diaries
|
|
313
328
|
rows = conn.execute(
|
|
314
329
|
"SELECT * FROM session_diary ORDER BY created_at DESC LIMIT ?",
|
|
315
330
|
(limit,),
|
|
316
331
|
).fetchall()
|
|
317
332
|
diaries = [dict(r) for r in rows]
|
|
318
|
-
return {"count": len(diaries), "sessions": diaries}
|
|
333
|
+
return {"count": len(diaries), "sessions": active, "diaries": diaries}
|
|
319
334
|
|
|
320
335
|
|
|
321
336
|
@app.get("/api/kg/nodes")
|
|
@@ -602,6 +617,11 @@ async def api_ops_execute(fid: str):
|
|
|
602
617
|
return JSONResponse({"error": f"Followup {fid} not found"}, status_code=404)
|
|
603
618
|
item = dict(row)
|
|
604
619
|
description = item["description"].replace('"', '\\"').replace("'", "\\'")
|
|
620
|
+
if platform.system() != "Darwin":
|
|
621
|
+
return JSONResponse(
|
|
622
|
+
{"error": "This operation requires macOS (uses osascript to open Terminal)"},
|
|
623
|
+
status_code=501,
|
|
624
|
+
)
|
|
605
625
|
script = f'tell application "Terminal" to do script "claude \\"NEXO: execute followup #{fid} — {description}\\""'
|
|
606
626
|
subprocess.Popen(["osascript", "-e", script])
|
|
607
627
|
return {"success": True, "followup_id": fid}
|
|
@@ -446,11 +446,12 @@
|
|
|
446
446
|
|
|
447
447
|
// --- Overdue Items ---
|
|
448
448
|
if (remindersData || followupsData) {
|
|
449
|
+
const excludeStatus = ['completed', 'COMPLETED', 'archived', 'deleted', 'DELETED', 'blocked', 'waiting'];
|
|
449
450
|
const reminders = (remindersData?.reminders || []).filter(r =>
|
|
450
|
-
r.status
|
|
451
|
+
!excludeStatus.includes(r.status) && r.date && r.date <= today
|
|
451
452
|
);
|
|
452
453
|
const followups = (followupsData?.followups || []).filter(f =>
|
|
453
|
-
f.status
|
|
454
|
+
!excludeStatus.includes(f.status) && f.date && f.date <= today
|
|
454
455
|
);
|
|
455
456
|
const total = reminders.length + followups.length;
|
|
456
457
|
const el = document.getElementById('overdue-count');
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/db/_core.py
CHANGED
|
@@ -9,11 +9,15 @@ import datetime
|
|
|
9
9
|
import pathlib
|
|
10
10
|
import threading
|
|
11
11
|
|
|
12
|
+
NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
|
|
13
|
+
_data_dir = os.path.join(NEXO_HOME, "data")
|
|
14
|
+
os.makedirs(_data_dir, exist_ok=True)
|
|
15
|
+
|
|
12
16
|
DB_PATH = os.environ.get(
|
|
13
17
|
"NEXO_TEST_DB",
|
|
14
18
|
os.environ.get(
|
|
15
19
|
"NEXO_DB",
|
|
16
|
-
os.path.join(
|
|
20
|
+
os.path.join(_data_dir, "nexo.db"),
|
|
17
21
|
),
|
|
18
22
|
)
|
|
19
23
|
|
package/src/db/_episodic.py
CHANGED
|
@@ -82,7 +82,7 @@ def auto_resolve_followups(change: dict) -> list[str]:
|
|
|
82
82
|
conn = get_db()
|
|
83
83
|
open_followups = conn.execute(
|
|
84
84
|
"SELECT * FROM followups WHERE status NOT LIKE 'COMPLETED%' "
|
|
85
|
-
"AND status
|
|
85
|
+
"AND status NOT IN ('DELETED','archived','blocked','waiting')"
|
|
86
86
|
).fetchall()
|
|
87
87
|
|
|
88
88
|
if not open_followups:
|
|
@@ -148,9 +148,7 @@ def update_change_commit(id: int, commit_ref: str) -> dict:
|
|
|
148
148
|
fts_upsert("change", str(id), r.get("files",""), body, "change_log", commit=False)
|
|
149
149
|
|
|
150
150
|
# Auto-resolve followups that match this change
|
|
151
|
-
|
|
152
|
-
if resolved:
|
|
153
|
-
r["auto_resolved_followups"] = resolved
|
|
151
|
+
r["_auto_resolved"] = auto_resolve_followups(r)
|
|
154
152
|
return r
|
|
155
153
|
|
|
156
154
|
|