nexo-brain 5.3.26 → 5.3.28
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/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/src/hook_guardrails.py +44 -0
- package/src/server.py +3 -0
- package/src/tools_sessions.py +6 -1
- package/src/dashboard/static/favicon 2.svg +0 -32
- package/src/dashboard/static/nexo-logo 2.png +0 -0
- package/src/dashboard/static/nexo-logo 2.svg +0 -40
- package/src/dashboard/static/style 2.css +0 -2458
- package/src/dashboard/templates/adaptive 2.html +0 -118
- package/src/dashboard/templates/artifacts 2.html +0 -133
- package/src/dashboard/templates/backups 2.html +0 -136
- package/src/dashboard/templates/base 2.html +0 -417
- package/src/dashboard/templates/calendar 2.html +0 -591
- package/src/dashboard/templates/chat 2.html +0 -356
- package/src/dashboard/templates/claims 2.html +0 -259
- package/src/dashboard/templates/cortex 2.html +0 -321
- package/src/dashboard/templates/credentials 2.html +0 -128
- package/src/dashboard/templates/crons 2.html +0 -370
- package/src/dashboard/templates/dashboard 2.html +0 -494
- package/src/dashboard/templates/dreams 2.html +0 -252
- package/src/dashboard/templates/email 2.html +0 -160
- package/src/dashboard/templates/evolution 2.html +0 -189
- package/src/dashboard/templates/feed 2.html +0 -249
- package/src/dashboard/templates/followup_health 2.html +0 -170
- package/src/dashboard/templates/graph 2.html +0 -201
- package/src/dashboard/templates/guard 2.html +0 -259
- package/src/dashboard/templates/inbox 2.html +0 -251
- package/src/dashboard/templates/memory 2.html +0 -420
- package/src/dashboard/templates/operations 2.html +0 -608
- package/src/dashboard/templates/plugins 2.html +0 -185
- package/src/dashboard/templates/protocol 2.html +0 -199
- package/src/dashboard/templates/rules 2.html +0 -246
- package/src/dashboard/templates/sentiment 2.html +0 -247
- package/src/dashboard/templates/sessions 2.html +0 -218
- package/src/dashboard/templates/skills 2.html +0 -329
- package/src/dashboard/templates/somatic 2.html +0 -73
- package/src/dashboard/templates/triggers 2.html +0 -133
- package/src/dashboard/templates/trust 2.html +0 -360
- package/src/db/__init__ 2.py +0 -259
- package/src/db/_core 2.py +0 -437
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_episodic 2.py +0 -762
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_goal_profiles 2.py +0 -376
- package/src/db/_hot_context 2.py +0 -660
- package/src/db/_outcomes 2.py +0 -800
- package/src/db/_personal_scripts 2.py +0 -582
- package/src/db/_sessions 2.py +0 -330
- package/src/db/_tasks 2.py +0 -91
- package/src/db/_watchers 2.py +0 -173
- package/src/doctor/formatters 2.py +0 -52
- package/src/doctor/models 2.py +0 -69
- package/src/doctor/planes 2.py +0 -87
- package/src/doctor/providers/__init__ 2.py +0 -1
- package/src/doctor/providers/deep 2.py +0 -367
- package/src/evolution_cycle 2.py +0 -519
- 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 -158
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/heartbeat-enforcement 2.py +0 -90
- package/src/hooks/heartbeat-posttool 2.sh +0 -18
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -152
- package/src/hooks/pre-compact 2.sh +0 -169
- package/src/hooks/protocol-guardrail 2.sh +0 -10
- package/src/hooks/protocol-pretool-guardrail 2.sh +0 -9
- package/src/hooks/session-stop 2.sh +0 -52
- package/src/kg_populate 2.py +0 -292
- package/src/maintenance 2.py +0 -53
- package/src/memory_backends 2.py +0 -71
- package/src/migrate_embeddings 2.py +0 -124
- package/src/nexo_sdk 2.py +0 -103
- package/src/observability 2.py +0 -199
- package/src/plugin_loader 2.py +0 -217
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -127
- package/src/plugins/claims_tools 2.py +0 -119
- package/src/plugins/cognitive_memory 2.py +0 -609
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -1155
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -560
- package/src/plugins/evolution 2.py +0 -167
- package/src/plugins/goal_engine 2.py +0 -142
- package/src/plugins/guard 2.py +0 -862
- package/src/plugins/impact 2.py +0 -29
- package/src/plugins/knowledge_graph_tools 2.py +0 -137
- package/src/plugins/media_memory_tools 2.py +0 -98
- package/src/plugins/memory_export 2.py +0 -196
- package/src/plugins/outcomes 2.py +0 -130
- package/src/plugins/personal_scripts 2.py +0 -117
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/protocol 2.py +0 -1449
- package/src/plugins/simple_api 2.py +0 -106
- package/src/plugins/skills 2.py +0 -341
- package/src/plugins/state_watchers 2.py +0 -79
- package/src/plugins/update 2.py +0 -986
- package/src/plugins/user_state_tools 2.py +0 -43
- package/src/plugins/workflow 2.py +0 -588
- package/src/protocol_settings 2.py +0 -59
- package/src/public_contribution 2.py +0 -466
- package/src/public_evolution_queue 2.py +0 -241
- package/src/requirements 2.txt +0 -14
- package/src/retroactive_learnings 2.py +0 -373
- 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/runtime_power 2.py +0 -874
- package/src/script_registry 2.py +0 -1559
- package/src/scripts/check-context 2.py +0 -272
- package/src/scripts/deep-sleep/apply_findings 2.py +0 -2327
- package/src/scripts/deep-sleep/collect 2.py +0 -928
- package/src/scripts/deep-sleep/extract 2.py +0 -330
- package/src/scripts/deep-sleep/extract-prompt 2.md +0 -285
- package/src/scripts/deep-sleep/synthesize 2.py +0 -312
- package/src/scripts/deep-sleep/synthesize-prompt 2.md +0 -336
- package/src/scripts/nexo-agent-run 2.py +0 -75
- 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 -300
- package/src/scripts/nexo-cognitive-decay 2.py +0 -257
- package/src/scripts/nexo-cortex-cycle 2.py +0 -293
- package/src/scripts/nexo-cron-wrapper 2.sh +0 -53
- package/src/scripts/nexo-daily-self-audit 2.py +0 -2161
- package/src/scripts/nexo-dashboard 2.sh +0 -29
- package/src/scripts/nexo-deep-sleep 2.sh +0 -86
- package/src/scripts/nexo-evolution-run 2.py +0 -1664
- package/src/scripts/nexo-followup-hygiene 2.py +0 -139
- package/src/scripts/nexo-hook-record 2.py +0 -42
- package/src/scripts/nexo-immune 2.py +0 -936
- package/src/scripts/nexo-impact-scorer 2.py +0 -117
- 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 -401
- package/src/scripts/nexo-learning-validator 2.py +0 -266
- package/src/scripts/nexo-migrate 2.py +0 -260
- package/src/scripts/nexo-outcome-checker 2.py +0 -127
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -456
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -35
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -354
- package/src/scripts/nexo-reflection 2.py +0 -256
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-sleep 2.py +0 -631
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-sync-clients 2.py +0 -16
- package/src/scripts/nexo-synthesis 2.py +0 -475
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -306
- package/src/scripts/nexo-watchdog 2.sh +0 -1207
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/scripts/rehydrate_learnings_from_archive 2.py +0 -245
- package/src/server 2.py +0 -1296
- package/src/skills/run-nexo-audit-phase/guide 2.md +0 -43
- package/src/skills/run-nexo-audit-phase/skill 2.json +0 -59
- package/src/skills/run-nexo-core-fix-cycle/guide 2.md +0 -17
- package/src/skills/run-nexo-core-fix-cycle/script 2.py +0 -276
- package/src/skills/run-nexo-core-fix-cycle/skill 2.json +0 -58
- package/src/skills/run-release-final-audit/guide 2.md +0 -16
- package/src/skills/run-release-final-audit/script 2.py +0 -259
- package/src/skills/run-release-final-audit/skill 2.json +0 -77
- package/src/skills/run-runtime-doctor/guide 2.md +0 -12
- package/src/skills/run-runtime-doctor/script 2.py +0 -21
- package/src/skills/run-runtime-doctor/skill 2.json +0 -25
- package/src/skills_runtime 2.py +0 -932
- package/src/state_watchers_runtime 2.py +0 -475
- package/src/storage_router 2.py +0 -32
- package/src/system_catalog 2.py +0 -786
- package/src/tools_coordination 2.py +0 -103
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_drive 2.py +0 -487
- package/src/tools_hot_context 2.py +0 -163
- package/src/tools_learnings 2.py +0 -612
- package/src/tools_menu 2.py +0 -229
- package/src/tools_reminders 2.py +0 -88
- package/src/tools_reminders_crud 2.py +0 -363
- package/src/tools_sessions 2.py +0 -1054
- package/src/tools_system_catalog 2.py +0 -19
- package/src/tools_task_history 2.py +0 -57
- package/src/tools_transcripts 2.py +0 -98
- package/src/transcript_utils 2.py +0 -412
- package/src/user_context 2.py +0 -46
- package/src/user_data_portability 2.py +0 -328
- package/src/user_state_model 2.py +0 -170
- package/templates/CLAUDE.md 2.template +0 -108
- package/templates/CODEX.AGENTS.md 2.template +0 -66
- package/templates/launchagents/README 2.md +0 -132
- package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +0 -39
- package/templates/launchagents/com.nexo.catchup 2.plist +0 -39
- package/templates/launchagents/com.nexo.cognitive-decay 2.plist +0 -40
- package/templates/launchagents/com.nexo.dashboard 2.plist +0 -43
- package/templates/launchagents/com.nexo.deep-sleep 2.plist +0 -43
- package/templates/launchagents/com.nexo.evolution 2.plist +0 -44
- package/templates/launchagents/com.nexo.followup-hygiene 2.plist +0 -45
- package/templates/launchagents/com.nexo.immune 2.plist +0 -41
- package/templates/launchagents/com.nexo.postmortem 2.plist +0 -45
- package/templates/launchagents/com.nexo.self-audit 2.plist +0 -47
- package/templates/launchagents/com.nexo.synthesis 2.plist +0 -45
- package/templates/launchagents/com.nexo.watchdog 2.plist +0 -37
- package/templates/nexo_helper 2.py +0 -301
- package/templates/openclaw 2.json +0 -13
- package/templates/plugin-template 2.py +0 -40
- package/templates/script-template 2.py +0 -59
- package/templates/script-template 2.sh +0 -13
- package/templates/skill-script-template 2.py +0 -48
- package/templates/skill-template 2.md +0 -33
|
@@ -1,928 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
"""
|
|
4
|
-
Deep Sleep v2 -- Phase 1: Collect all context for overnight analysis.
|
|
5
|
-
|
|
6
|
-
Gathers transcripts, DB data, logs, and discovered files into a single
|
|
7
|
-
plain-text context file that subsequent phases read via the configured
|
|
8
|
-
automation backend.
|
|
9
|
-
|
|
10
|
-
Environment variables:
|
|
11
|
-
NEXO_HOME -- root of the NEXO installation (default: ~/.nexo)
|
|
12
|
-
NEXO_CODE -- path to the NEXO source repo (optional, for self-analysis)
|
|
13
|
-
"""
|
|
14
|
-
import json
|
|
15
|
-
import os
|
|
16
|
-
import re
|
|
17
|
-
import sqlite3
|
|
18
|
-
import sys
|
|
19
|
-
from collections import Counter
|
|
20
|
-
from datetime import datetime, timedelta
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
|
|
23
|
-
_DEFAULT_RUNTIME_ROOT = Path(__file__).resolve().parents[2]
|
|
24
|
-
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_DEFAULT_RUNTIME_ROOT)))
|
|
25
|
-
if str(NEXO_CODE) not in sys.path:
|
|
26
|
-
sys.path.insert(0, str(NEXO_CODE))
|
|
27
|
-
|
|
28
|
-
import transcript_utils as _transcripts
|
|
29
|
-
|
|
30
|
-
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
31
|
-
DEEP_SLEEP_DIR = NEXO_HOME / "operations" / "deep-sleep"
|
|
32
|
-
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
33
|
-
COGNITIVE_DB = NEXO_HOME / "data" / "cognitive.db"
|
|
34
|
-
_TABLE_COLUMNS_CACHE: dict[tuple[str, str], set[str]] = {}
|
|
35
|
-
|
|
36
|
-
MIN_USER_MESSAGES = 3 # Skip trivial sessions
|
|
37
|
-
|
|
38
|
-
# Patterns that indicate sensitive data (passwords, tokens, API keys, etc.)
|
|
39
|
-
_SENSITIVE_PATTERNS = re.compile(
|
|
40
|
-
r'(?:'
|
|
41
|
-
r'sk-ant-[A-Za-z0-9_-]+' # Anthropic API keys
|
|
42
|
-
r'|shpat_[A-Fa-f0-9]+' # Shopify admin tokens
|
|
43
|
-
r'|shpss_[A-Fa-f0-9]+' # Shopify shared secret
|
|
44
|
-
r'|sk-[A-Za-z0-9]{20,}' # OpenAI-style keys
|
|
45
|
-
r'|ghp_[A-Za-z0-9]{36,}' # GitHub PATs
|
|
46
|
-
r'|gho_[A-Za-z0-9]{36,}' # GitHub OAuth tokens
|
|
47
|
-
r'|AIza[A-Za-z0-9_-]{35}' # Google API keys
|
|
48
|
-
r'|ya29\.[A-Za-z0-9_-]+' # Google OAuth tokens
|
|
49
|
-
r'|xox[bpsa]-[A-Za-z0-9-]+' # Slack tokens
|
|
50
|
-
r'|EAAG[A-Za-z0-9]+' # Meta/Facebook tokens
|
|
51
|
-
r'|[Pp]assword\s*[:=]\s*\S+' # password: value or password=value
|
|
52
|
-
r'|[Ss]ecret\s*[:=]\s*\S+' # secret: value
|
|
53
|
-
r'|[Tt]oken\s*[:=]\s*\S+' # token: value
|
|
54
|
-
r'|[Aa]pi[_-]?[Kk]ey\s*[:=]\s*\S+' # api_key: value
|
|
55
|
-
r')'
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _redact_sensitive(text: str) -> str:
|
|
60
|
-
"""Replace sensitive patterns in text with [REDACTED]."""
|
|
61
|
-
return _SENSITIVE_PATTERNS.sub('[REDACTED]', text)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# ── Transcript collection (Claude Code + Codex) ────────────────────────────
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def _session_identifier(client: str, session_file: str) -> str:
|
|
68
|
-
return f"{client}:{session_file}"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def find_claude_session_files() -> list[Path]:
|
|
72
|
-
"""Find Claude Code session JSONL files under ~/.claude/projects."""
|
|
73
|
-
return _transcripts.find_claude_session_files()
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def find_codex_session_files() -> list[Path]:
|
|
77
|
-
"""Find Codex session JSONL files under ~/.codex/sessions and archived_sessions."""
|
|
78
|
-
return _transcripts.find_codex_session_files()
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def extract_claude_session(jsonl_path: Path) -> dict | None:
|
|
82
|
-
"""Extract clean transcript from a Claude Code JSONL session."""
|
|
83
|
-
return _transcripts.extract_claude_session(jsonl_path)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def extract_codex_session(jsonl_path: Path) -> dict | None:
|
|
87
|
-
"""Extract clean transcript from a Codex JSONL session."""
|
|
88
|
-
return _transcripts.extract_codex_session(jsonl_path)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def collect_transcripts_since(since_iso: str, until_iso: str = "") -> list[dict]:
|
|
92
|
-
"""Collect all sessions modified after `since_iso` (exclusive) up to `until_iso` (inclusive).
|
|
93
|
-
|
|
94
|
-
Uses a watermark approach: deep sleep tracks the last processed timestamp
|
|
95
|
-
so nothing is missed regardless of when sessions happen (day, night, etc.).
|
|
96
|
-
"""
|
|
97
|
-
return _transcripts.collect_transcripts_since(since_iso, until_iso)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
# ── Database queries ──────────────────────────────────────────────────────
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def safe_query(db_path: Path, query: str, params: tuple = ()) -> list[dict]:
|
|
104
|
-
"""Run a query and return rows as dicts. Returns [] on any error."""
|
|
105
|
-
if not db_path.exists():
|
|
106
|
-
return []
|
|
107
|
-
try:
|
|
108
|
-
conn = sqlite3.connect(str(db_path))
|
|
109
|
-
conn.row_factory = sqlite3.Row
|
|
110
|
-
rows = conn.execute(query, params).fetchall()
|
|
111
|
-
result = [dict(r) for r in rows]
|
|
112
|
-
conn.close()
|
|
113
|
-
return result
|
|
114
|
-
except Exception as e:
|
|
115
|
-
print(f" [collect] DB query error ({db_path.name}): {e}", file=sys.stderr)
|
|
116
|
-
return []
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def _table_columns(db_path: Path, table_name: str) -> set[str]:
|
|
120
|
-
cache_key = (str(db_path), table_name)
|
|
121
|
-
cached = _TABLE_COLUMNS_CACHE.get(cache_key)
|
|
122
|
-
if cached is not None:
|
|
123
|
-
return cached
|
|
124
|
-
if not db_path.exists():
|
|
125
|
-
_TABLE_COLUMNS_CACHE[cache_key] = set()
|
|
126
|
-
return set()
|
|
127
|
-
try:
|
|
128
|
-
conn = sqlite3.connect(str(db_path))
|
|
129
|
-
conn.row_factory = sqlite3.Row
|
|
130
|
-
rows = conn.execute(f"PRAGMA table_info({table_name})").fetchall()
|
|
131
|
-
conn.close()
|
|
132
|
-
except Exception:
|
|
133
|
-
_TABLE_COLUMNS_CACHE[cache_key] = set()
|
|
134
|
-
return set()
|
|
135
|
-
columns = {str(row["name"]) for row in rows}
|
|
136
|
-
_TABLE_COLUMNS_CACHE[cache_key] = columns
|
|
137
|
-
return columns
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def _optional_column_sql(db_path: Path, table_name: str, column_name: str, default_sql: str = "''") -> str:
|
|
141
|
-
if column_name in _table_columns(db_path, table_name):
|
|
142
|
-
return column_name
|
|
143
|
-
return f"{default_sql} AS {column_name}"
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def collect_followups() -> list[dict]:
|
|
147
|
-
"""Active followups from nexo.db."""
|
|
148
|
-
return safe_query(
|
|
149
|
-
NEXO_DB,
|
|
150
|
-
"SELECT * FROM followups WHERE status NOT IN ('COMPLETED', 'CANCELLED') ORDER BY date ASC"
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def collect_learnings() -> list[dict]:
|
|
155
|
-
"""Active learnings from nexo.db."""
|
|
156
|
-
return safe_query(NEXO_DB, "SELECT * FROM learnings ORDER BY updated_at DESC LIMIT 200")
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def collect_diaries(target_date: str) -> list[dict]:
|
|
160
|
-
"""Today's session diaries."""
|
|
161
|
-
# Diaries store created_at as unix timestamp or ISO string -- handle both
|
|
162
|
-
start_ts = datetime.strptime(target_date, "%Y-%m-%d").timestamp()
|
|
163
|
-
end_ts = start_ts + 86400
|
|
164
|
-
rows = safe_query(
|
|
165
|
-
NEXO_DB,
|
|
166
|
-
"SELECT * FROM session_diary WHERE created_at >= ? AND created_at < ? ORDER BY created_at ASC",
|
|
167
|
-
(start_ts, end_ts)
|
|
168
|
-
)
|
|
169
|
-
if not rows:
|
|
170
|
-
# Try ISO format
|
|
171
|
-
rows = safe_query(
|
|
172
|
-
NEXO_DB,
|
|
173
|
-
"SELECT * FROM session_diary WHERE created_at >= ? AND created_at < ? ORDER BY created_at ASC",
|
|
174
|
-
(target_date + "T00:00:00", target_date + "T23:59:59")
|
|
175
|
-
)
|
|
176
|
-
return rows
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def collect_trust_score() -> list[dict]:
|
|
180
|
-
"""Current trust score and 7-day history from cognitive.db."""
|
|
181
|
-
return safe_query(
|
|
182
|
-
COGNITIVE_DB,
|
|
183
|
-
"SELECT * FROM trust_score ORDER BY rowid DESC LIMIT 1"
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def _parse_diary_created_at(value) -> datetime | None:
|
|
188
|
-
if value in (None, ""):
|
|
189
|
-
return None
|
|
190
|
-
try:
|
|
191
|
-
if isinstance(value, (int, float)) or (isinstance(value, str) and str(value).strip().isdigit()):
|
|
192
|
-
return datetime.fromtimestamp(float(value))
|
|
193
|
-
except Exception:
|
|
194
|
-
return None
|
|
195
|
-
try:
|
|
196
|
-
return datetime.fromisoformat(str(value).replace("Z", "+00:00").replace("+00:00", ""))
|
|
197
|
-
except Exception:
|
|
198
|
-
return None
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def _sample_evenly(rows: list[dict], limit: int) -> list[dict]:
|
|
202
|
-
if limit <= 0 or not rows:
|
|
203
|
-
return []
|
|
204
|
-
if len(rows) <= limit:
|
|
205
|
-
return list(rows)
|
|
206
|
-
if limit == 1:
|
|
207
|
-
return [rows[-1]]
|
|
208
|
-
step = (len(rows) - 1) / float(limit - 1)
|
|
209
|
-
indices = sorted({round(i * step) for i in range(limit)})
|
|
210
|
-
sampled = [rows[idx] for idx in indices]
|
|
211
|
-
i = 0
|
|
212
|
-
while len(sampled) < limit and i < len(rows):
|
|
213
|
-
if rows[i] not in sampled:
|
|
214
|
-
sampled.append(rows[i])
|
|
215
|
-
i += 1
|
|
216
|
-
return sampled[:limit]
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _compact_diary_row(row: dict) -> dict:
|
|
220
|
-
created = _parse_diary_created_at(row.get("created_at"))
|
|
221
|
-
return {
|
|
222
|
-
"session_id": row.get("session_id", ""),
|
|
223
|
-
"created_at": created.isoformat() if created else str(row.get("created_at", "")),
|
|
224
|
-
"domain": row.get("domain", "") or "",
|
|
225
|
-
"mental_state": row.get("mental_state", "") or "",
|
|
226
|
-
"summary": str(row.get("summary", "") or "")[:240],
|
|
227
|
-
"self_critique": str(row.get("self_critique", "") or "")[:240],
|
|
228
|
-
"source": row.get("source", "") or "",
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def _load_project_aliases() -> dict[str, set[str]]:
|
|
233
|
-
atlas_path = NEXO_HOME / "brain" / "project-atlas.json"
|
|
234
|
-
aliases: dict[str, set[str]] = {}
|
|
235
|
-
if not atlas_path.is_file():
|
|
236
|
-
return aliases
|
|
237
|
-
try:
|
|
238
|
-
payload = json.loads(atlas_path.read_text())
|
|
239
|
-
except Exception:
|
|
240
|
-
return aliases
|
|
241
|
-
if not isinstance(payload, dict):
|
|
242
|
-
return aliases
|
|
243
|
-
for key, value in payload.items():
|
|
244
|
-
if str(key).startswith("_"):
|
|
245
|
-
continue
|
|
246
|
-
canonical = str(key).strip().lower()
|
|
247
|
-
alias_set = {canonical, canonical.replace("-", " "), canonical.replace("_", " ")}
|
|
248
|
-
if isinstance(value, dict):
|
|
249
|
-
for alias in value.get("aliases", []) or []:
|
|
250
|
-
alias_value = str(alias or "").strip().lower()
|
|
251
|
-
if alias_value:
|
|
252
|
-
alias_set.add(alias_value)
|
|
253
|
-
alias_set.add(alias_value.replace("-", " "))
|
|
254
|
-
aliases[canonical] = {item for item in alias_set if item}
|
|
255
|
-
return aliases
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def _match_projects(text: str, alias_map: dict[str, set[str]]) -> set[str]:
|
|
259
|
-
haystack = str(text or "").strip().lower()
|
|
260
|
-
if not haystack:
|
|
261
|
-
return set()
|
|
262
|
-
matches: set[str] = set()
|
|
263
|
-
for canonical, aliases in alias_map.items():
|
|
264
|
-
for alias in sorted(aliases, key=len, reverse=True):
|
|
265
|
-
if alias and alias in haystack:
|
|
266
|
-
matches.add(canonical)
|
|
267
|
-
break
|
|
268
|
-
return matches
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def _priority_weight(value) -> float:
|
|
272
|
-
lowered = str(value or "").strip().lower()
|
|
273
|
-
if lowered in {"critical", "urgent"}:
|
|
274
|
-
return 4.0
|
|
275
|
-
if lowered == "high":
|
|
276
|
-
return 3.0
|
|
277
|
-
if lowered == "medium":
|
|
278
|
-
return 2.0
|
|
279
|
-
if lowered == "low":
|
|
280
|
-
return 1.0
|
|
281
|
-
return 1.5
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
def _compact_periodic_summary(data: dict) -> dict:
|
|
285
|
-
return {
|
|
286
|
-
"label": data.get("label", ""),
|
|
287
|
-
"window_start": data.get("window_start", ""),
|
|
288
|
-
"window_end": data.get("window_end", ""),
|
|
289
|
-
"summary": str(data.get("summary", "") or "")[:320],
|
|
290
|
-
"top_projects": data.get("top_projects", [])[:4],
|
|
291
|
-
"top_patterns": data.get("top_patterns", [])[:4],
|
|
292
|
-
"avg_mood_score": data.get("avg_mood_score"),
|
|
293
|
-
"avg_trust_score": data.get("avg_trust_score"),
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
def _load_periodic_summaries(target_date: str, *, kind: str, limit: int = 2) -> list[dict]:
|
|
298
|
-
target_day = datetime.strptime(target_date, "%Y-%m-%d")
|
|
299
|
-
summaries: list[tuple[str, dict]] = []
|
|
300
|
-
pattern = "*-weekly-summary.json" if kind == "weekly" else "*-monthly-summary.json"
|
|
301
|
-
for path in sorted(DEEP_SLEEP_DIR.glob(pattern)):
|
|
302
|
-
try:
|
|
303
|
-
payload = json.loads(path.read_text())
|
|
304
|
-
except Exception:
|
|
305
|
-
continue
|
|
306
|
-
window_end_raw = str(payload.get("window_end", "") or "")
|
|
307
|
-
parsed = _parse_diary_created_at(window_end_raw)
|
|
308
|
-
if parsed and parsed >= target_day:
|
|
309
|
-
continue
|
|
310
|
-
summaries.append((window_end_raw, _compact_periodic_summary(payload)))
|
|
311
|
-
summaries.sort(key=lambda item: item[0], reverse=True)
|
|
312
|
-
return [item for _, item in summaries[:limit]]
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
def _project_priority_signals(target_day: datetime, compact_diaries: list[dict]) -> list[dict]:
|
|
316
|
-
alias_map = _load_project_aliases()
|
|
317
|
-
scoreboard: dict[str, dict] = {}
|
|
318
|
-
|
|
319
|
-
def bump(project: str, score: float, signal_key: str, reason: str) -> None:
|
|
320
|
-
if not project:
|
|
321
|
-
return
|
|
322
|
-
slot = scoreboard.setdefault(
|
|
323
|
-
project,
|
|
324
|
-
{
|
|
325
|
-
"project": project,
|
|
326
|
-
"score": 0.0,
|
|
327
|
-
"signals": {
|
|
328
|
-
"diary_sessions": 0,
|
|
329
|
-
"learnings": 0,
|
|
330
|
-
"followups": 0,
|
|
331
|
-
"decisions": 0,
|
|
332
|
-
},
|
|
333
|
-
"reasons": [],
|
|
334
|
-
},
|
|
335
|
-
)
|
|
336
|
-
slot["score"] += score
|
|
337
|
-
slot["signals"][signal_key] += 1
|
|
338
|
-
if reason and reason not in slot["reasons"]:
|
|
339
|
-
slot["reasons"].append(reason)
|
|
340
|
-
|
|
341
|
-
for row in compact_diaries:
|
|
342
|
-
created = _parse_diary_created_at(row.get("created_at"))
|
|
343
|
-
recency_bonus = 1.0
|
|
344
|
-
if created:
|
|
345
|
-
age_days = max(0.0, (target_day - created).total_seconds() / 86400)
|
|
346
|
-
recency_bonus = 1.4 if age_days <= 7 else 1.0
|
|
347
|
-
candidates = set()
|
|
348
|
-
domain = str(row.get("domain", "") or "").strip().lower()
|
|
349
|
-
if domain:
|
|
350
|
-
candidates.add(domain)
|
|
351
|
-
candidates |= _match_projects(" ".join([row.get("summary", ""), row.get("self_critique", "")]), alias_map)
|
|
352
|
-
for project in candidates:
|
|
353
|
-
bump(project, 3.0 * recency_bonus, "diary_sessions", "recent session diary activity")
|
|
354
|
-
|
|
355
|
-
learning_priority_sql = _optional_column_sql(NEXO_DB, "learnings", "priority", "'medium'")
|
|
356
|
-
learning_weight_sql = _optional_column_sql(NEXO_DB, "learnings", "weight", "0")
|
|
357
|
-
learning_applies_sql = _optional_column_sql(NEXO_DB, "learnings", "applies_to", "''")
|
|
358
|
-
learning_rows = safe_query(
|
|
359
|
-
NEXO_DB,
|
|
360
|
-
f"SELECT category, title, content, created_at, updated_at, {learning_priority_sql}, "
|
|
361
|
-
f"{learning_weight_sql}, {learning_applies_sql} FROM learnings "
|
|
362
|
-
"ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 160",
|
|
363
|
-
)
|
|
364
|
-
for row in learning_rows:
|
|
365
|
-
text = " ".join(
|
|
366
|
-
[
|
|
367
|
-
str(row.get("applies_to", "") or ""),
|
|
368
|
-
str(row.get("title", "") or ""),
|
|
369
|
-
str(row.get("content", "") or ""),
|
|
370
|
-
str(row.get("category", "") or ""),
|
|
371
|
-
]
|
|
372
|
-
)
|
|
373
|
-
matched = _match_projects(text, alias_map)
|
|
374
|
-
if not matched:
|
|
375
|
-
continue
|
|
376
|
-
weight = float(row.get("weight", 0) or 0)
|
|
377
|
-
score = 1.0 + _priority_weight(row.get("priority")) + min(2.0, max(0.0, weight))
|
|
378
|
-
for project in matched:
|
|
379
|
-
bump(project, score, "learnings", "recent leverage-bearing learning")
|
|
380
|
-
|
|
381
|
-
followup_priority_sql = _optional_column_sql(NEXO_DB, "followups", "priority", "'medium'")
|
|
382
|
-
followup_reasoning_sql = _optional_column_sql(NEXO_DB, "followups", "reasoning", "''")
|
|
383
|
-
followup_rows = safe_query(
|
|
384
|
-
NEXO_DB,
|
|
385
|
-
f"SELECT id, description, date, status, {followup_priority_sql}, created_at, updated_at, "
|
|
386
|
-
f"{followup_reasoning_sql} FROM followups "
|
|
387
|
-
"WHERE status NOT IN ('COMPLETED', 'CANCELLED') ORDER BY date ASC, created_at ASC LIMIT 120",
|
|
388
|
-
)
|
|
389
|
-
for row in followup_rows:
|
|
390
|
-
matched = _match_projects(
|
|
391
|
-
" ".join(
|
|
392
|
-
[
|
|
393
|
-
str(row.get("description", "") or ""),
|
|
394
|
-
str(row.get("reasoning", "") or ""),
|
|
395
|
-
]
|
|
396
|
-
),
|
|
397
|
-
alias_map,
|
|
398
|
-
)
|
|
399
|
-
if not matched:
|
|
400
|
-
continue
|
|
401
|
-
overdue_bonus = 0.0
|
|
402
|
-
due_value = str(row.get("date", "") or "")
|
|
403
|
-
try:
|
|
404
|
-
if due_value:
|
|
405
|
-
due_dt = datetime.strptime(due_value[:10], "%Y-%m-%d")
|
|
406
|
-
if due_dt <= target_day:
|
|
407
|
-
overdue_bonus = 1.5
|
|
408
|
-
except Exception:
|
|
409
|
-
overdue_bonus = 0.0
|
|
410
|
-
score = 1.5 + _priority_weight(row.get("priority")) + overdue_bonus
|
|
411
|
-
for project in matched:
|
|
412
|
-
bump(project, score, "followups", "open followup pressure")
|
|
413
|
-
|
|
414
|
-
decision_status_sql = _optional_column_sql(NEXO_DB, "decisions", "status", "''")
|
|
415
|
-
decision_reasoning_sql = _optional_column_sql(NEXO_DB, "decisions", "reasoning", "''")
|
|
416
|
-
decision_review_due_sql = _optional_column_sql(NEXO_DB, "decisions", "review_due_at", "NULL")
|
|
417
|
-
decision_rows = safe_query(
|
|
418
|
-
NEXO_DB,
|
|
419
|
-
f"SELECT domain, outcome, {decision_status_sql}, {decision_reasoning_sql}, decision, based_on, created_at, "
|
|
420
|
-
f"{decision_review_due_sql} FROM decisions "
|
|
421
|
-
"ORDER BY COALESCE(created_at, review_due_at) DESC LIMIT 120",
|
|
422
|
-
)
|
|
423
|
-
for row in decision_rows:
|
|
424
|
-
matched = set()
|
|
425
|
-
domain = str(row.get("domain", "") or "").strip().lower()
|
|
426
|
-
if domain:
|
|
427
|
-
matched.add(domain)
|
|
428
|
-
matched |= _match_projects(
|
|
429
|
-
" ".join(
|
|
430
|
-
[
|
|
431
|
-
str(row.get("reasoning", "") or ""),
|
|
432
|
-
str(row.get("decision", "") or ""),
|
|
433
|
-
str(row.get("based_on", "") or ""),
|
|
434
|
-
str(row.get("outcome", "") or ""),
|
|
435
|
-
str(row.get("status", "") or ""),
|
|
436
|
-
]
|
|
437
|
-
),
|
|
438
|
-
alias_map,
|
|
439
|
-
)
|
|
440
|
-
if not matched:
|
|
441
|
-
continue
|
|
442
|
-
outcome = str(row.get("outcome", "") or "").lower()
|
|
443
|
-
status = str(row.get("status", "") or "").lower()
|
|
444
|
-
score = 2.5
|
|
445
|
-
if any(token in outcome for token in ("fail", "error", "blocked", "regression")):
|
|
446
|
-
score += 2.0
|
|
447
|
-
if status in {"pending", "blocked", "open"}:
|
|
448
|
-
score += 1.5
|
|
449
|
-
for project in matched:
|
|
450
|
-
bump(project, score, "decisions", "recent decision pressure")
|
|
451
|
-
|
|
452
|
-
ranked = sorted(scoreboard.values(), key=lambda item: item["score"], reverse=True)
|
|
453
|
-
for item in ranked:
|
|
454
|
-
item["score"] = round(item["score"], 2)
|
|
455
|
-
item["reasons"] = item["reasons"][:4]
|
|
456
|
-
return ranked[:8]
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
def collect_long_horizon_context(
|
|
460
|
-
target_date: str,
|
|
461
|
-
*,
|
|
462
|
-
horizon_days: int = 60,
|
|
463
|
-
recent_days: int = 14,
|
|
464
|
-
max_diaries: int = 20,
|
|
465
|
-
max_sessions: int = 12,
|
|
466
|
-
) -> dict:
|
|
467
|
-
"""Build long-horizon context blending recent and older evidence.
|
|
468
|
-
|
|
469
|
-
Strategy:
|
|
470
|
-
- recent 70% from the last `recent_days`
|
|
471
|
-
- older 30% sampled evenly from the rest of the `horizon_days` window
|
|
472
|
-
"""
|
|
473
|
-
target_day = datetime.strptime(target_date, "%Y-%m-%d")
|
|
474
|
-
horizon_start = target_day - timedelta(days=horizon_days)
|
|
475
|
-
recent_start = target_day - timedelta(days=recent_days)
|
|
476
|
-
|
|
477
|
-
diary_rows = safe_query(
|
|
478
|
-
NEXO_DB,
|
|
479
|
-
"SELECT session_id, created_at, summary, mental_state, domain, self_critique, source "
|
|
480
|
-
"FROM session_diary ORDER BY created_at ASC"
|
|
481
|
-
)
|
|
482
|
-
compact_diaries = []
|
|
483
|
-
for row in diary_rows:
|
|
484
|
-
created = _parse_diary_created_at(row.get("created_at"))
|
|
485
|
-
if not created:
|
|
486
|
-
continue
|
|
487
|
-
if not (horizon_start <= created < target_day):
|
|
488
|
-
continue
|
|
489
|
-
compact_diaries.append(_compact_diary_row(row))
|
|
490
|
-
|
|
491
|
-
recent_diaries = [row for row in compact_diaries if _parse_diary_created_at(row.get("created_at")) and _parse_diary_created_at(row.get("created_at")) >= recent_start]
|
|
492
|
-
older_diaries = [row for row in compact_diaries if row not in recent_diaries]
|
|
493
|
-
recent_quota = max(1, round(max_diaries * 0.7))
|
|
494
|
-
older_quota = max(0, max_diaries - recent_quota)
|
|
495
|
-
sampled_diaries = recent_diaries[-recent_quota:] + _sample_evenly(older_diaries, older_quota)
|
|
496
|
-
sampled_diaries.sort(key=lambda row: row.get("created_at", ""))
|
|
497
|
-
|
|
498
|
-
recurring_domains = Counter(row["domain"] for row in compact_diaries if row.get("domain"))
|
|
499
|
-
recurring_states = Counter(row["mental_state"] for row in compact_diaries if row.get("mental_state"))
|
|
500
|
-
recurring_critiques = Counter(row["self_critique"] for row in compact_diaries if row.get("self_critique"))
|
|
501
|
-
|
|
502
|
-
learning_reasoning_sql = _optional_column_sql(NEXO_DB, "learnings", "reasoning", "''")
|
|
503
|
-
learning_prevention_sql = _optional_column_sql(NEXO_DB, "learnings", "prevention", "''")
|
|
504
|
-
learning_applies_sql = _optional_column_sql(NEXO_DB, "learnings", "applies_to", "''")
|
|
505
|
-
learning_rows = safe_query(
|
|
506
|
-
NEXO_DB,
|
|
507
|
-
f"SELECT category, title, content, created_at, updated_at, {learning_reasoning_sql}, "
|
|
508
|
-
f"{learning_prevention_sql}, {learning_applies_sql} "
|
|
509
|
-
"FROM learnings ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 120"
|
|
510
|
-
)
|
|
511
|
-
long_horizon_learnings = []
|
|
512
|
-
for row in learning_rows:
|
|
513
|
-
long_horizon_learnings.append({
|
|
514
|
-
"category": row.get("category", ""),
|
|
515
|
-
"title": str(row.get("title", "") or "")[:140],
|
|
516
|
-
"content": str(row.get("content", "") or "")[:260],
|
|
517
|
-
"reasoning": str(row.get("reasoning", "") or "")[:180],
|
|
518
|
-
"prevention": str(row.get("prevention", "") or "")[:180],
|
|
519
|
-
"applies_to": str(row.get("applies_to", "") or "")[:180],
|
|
520
|
-
"updated_at": str(row.get("updated_at", "") or row.get("created_at", "")),
|
|
521
|
-
})
|
|
522
|
-
long_horizon_learnings = long_horizon_learnings[:24]
|
|
523
|
-
|
|
524
|
-
transcript_candidates: list[dict] = []
|
|
525
|
-
transcript_files: list[tuple[str, Path]] = [
|
|
526
|
-
("claude_code", path) for path in find_claude_session_files()
|
|
527
|
-
] + [
|
|
528
|
-
("codex", path) for path in find_codex_session_files()
|
|
529
|
-
]
|
|
530
|
-
horizon_end = target_day
|
|
531
|
-
for client, path in transcript_files:
|
|
532
|
-
try:
|
|
533
|
-
modified = datetime.fromtimestamp(path.stat().st_mtime)
|
|
534
|
-
except OSError:
|
|
535
|
-
continue
|
|
536
|
-
if not (horizon_start <= modified < horizon_end):
|
|
537
|
-
continue
|
|
538
|
-
transcript_candidates.append({
|
|
539
|
-
"client": client,
|
|
540
|
-
"session_file": _session_identifier(client, path.name),
|
|
541
|
-
"modified": modified.isoformat(),
|
|
542
|
-
"session_path": str(path),
|
|
543
|
-
})
|
|
544
|
-
transcript_candidates.sort(key=lambda row: row["modified"])
|
|
545
|
-
recent_sessions = [row for row in transcript_candidates if datetime.fromisoformat(row["modified"]) >= recent_start]
|
|
546
|
-
older_sessions = [row for row in transcript_candidates if row not in recent_sessions]
|
|
547
|
-
recent_session_quota = max(1, round(max_sessions * 0.7))
|
|
548
|
-
older_session_quota = max(0, max_sessions - recent_session_quota)
|
|
549
|
-
sampled_sessions = recent_sessions[-recent_session_quota:] + _sample_evenly(older_sessions, older_session_quota)
|
|
550
|
-
sampled_sessions.sort(key=lambda row: row["modified"])
|
|
551
|
-
|
|
552
|
-
stale_followups = safe_query(
|
|
553
|
-
NEXO_DB,
|
|
554
|
-
"SELECT id, description, date, status, created_at, updated_at FROM followups "
|
|
555
|
-
"WHERE status NOT IN ('COMPLETED', 'CANCELLED') ORDER BY date ASC, created_at ASC LIMIT 50"
|
|
556
|
-
)
|
|
557
|
-
older_than_week = []
|
|
558
|
-
week_ago = target_day - timedelta(days=7)
|
|
559
|
-
for row in stale_followups:
|
|
560
|
-
created = _parse_diary_created_at(row.get("created_at"))
|
|
561
|
-
if created and created < week_ago:
|
|
562
|
-
older_than_week.append({
|
|
563
|
-
"id": row.get("id", ""),
|
|
564
|
-
"description": str(row.get("description", "") or "")[:180],
|
|
565
|
-
"date": row.get("date", ""),
|
|
566
|
-
"status": row.get("status", ""),
|
|
567
|
-
"created_at": created.isoformat(),
|
|
568
|
-
})
|
|
569
|
-
|
|
570
|
-
weekly_summaries = _load_periodic_summaries(target_date, kind="weekly", limit=2)
|
|
571
|
-
monthly_summaries = _load_periodic_summaries(target_date, kind="monthly", limit=2)
|
|
572
|
-
project_priority_signals = _project_priority_signals(target_day, compact_diaries)
|
|
573
|
-
|
|
574
|
-
return {
|
|
575
|
-
"horizon_days": horizon_days,
|
|
576
|
-
"recent_window_days": recent_days,
|
|
577
|
-
"sample_strategy": "70% recent + 30% older evenly sampled",
|
|
578
|
-
"historical_diaries": sampled_diaries,
|
|
579
|
-
"historical_sessions": sampled_sessions,
|
|
580
|
-
"historical_learnings": long_horizon_learnings,
|
|
581
|
-
"recurring_domains": recurring_domains.most_common(8),
|
|
582
|
-
"recurring_mental_states": recurring_states.most_common(8),
|
|
583
|
-
"recurring_self_critiques": recurring_critiques.most_common(6),
|
|
584
|
-
"stale_followups": older_than_week[:12],
|
|
585
|
-
"project_priority_signals": project_priority_signals,
|
|
586
|
-
"weekly_summaries": weekly_summaries,
|
|
587
|
-
"monthly_summaries": monthly_summaries,
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
# ── Discovery: scan NEXO_HOME for non-core content ───────────────────────
|
|
592
|
-
|
|
593
|
-
CORE_DIRS = {"data", "operations", "logs", "coordination", "brain"}
|
|
594
|
-
CORE_FILES = {"config.json", "nexo.db", "cognitive.db"}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
def discover_extras() -> list[dict]:
|
|
598
|
-
"""Scan NEXO_HOME for non-core directories and files."""
|
|
599
|
-
extras = []
|
|
600
|
-
if not NEXO_HOME.exists():
|
|
601
|
-
return extras
|
|
602
|
-
|
|
603
|
-
for item in sorted(NEXO_HOME.iterdir()):
|
|
604
|
-
name = item.name
|
|
605
|
-
if name.startswith("."):
|
|
606
|
-
continue
|
|
607
|
-
if name in CORE_DIRS or name in CORE_FILES:
|
|
608
|
-
continue
|
|
609
|
-
|
|
610
|
-
entry = {"name": name, "path": str(item), "type": "dir" if item.is_dir() else "file"}
|
|
611
|
-
|
|
612
|
-
if item.is_dir():
|
|
613
|
-
# Count contents and list interesting files
|
|
614
|
-
files = list(item.rglob("*"))
|
|
615
|
-
entry["file_count"] = len([f for f in files if f.is_file()])
|
|
616
|
-
entry["notable_files"] = [
|
|
617
|
-
str(f.relative_to(item))
|
|
618
|
-
for f in files
|
|
619
|
-
if f.is_file() and f.suffix in (".py", ".sh", ".json", ".db", ".log", ".sqlite")
|
|
620
|
-
][:20]
|
|
621
|
-
elif item.is_file():
|
|
622
|
-
entry["size"] = item.stat().st_size
|
|
623
|
-
|
|
624
|
-
extras.append(entry)
|
|
625
|
-
|
|
626
|
-
return extras
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
# ── LaunchAgent logs ──────────────────────────────────────────────────────
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
def collect_error_logs(target_date: str) -> list[dict]:
|
|
633
|
-
"""Scan NEXO_HOME/logs/ for lines containing errors from today."""
|
|
634
|
-
log_dir = NEXO_HOME / "logs"
|
|
635
|
-
if not log_dir.exists():
|
|
636
|
-
return []
|
|
637
|
-
|
|
638
|
-
errors = []
|
|
639
|
-
for log_file in sorted(log_dir.glob("*.log")):
|
|
640
|
-
try:
|
|
641
|
-
lines = log_file.read_text(errors="replace").splitlines()
|
|
642
|
-
except Exception:
|
|
643
|
-
continue
|
|
644
|
-
|
|
645
|
-
file_errors = []
|
|
646
|
-
for i, line in enumerate(lines):
|
|
647
|
-
# Match lines from today that contain error indicators
|
|
648
|
-
if target_date in line and any(
|
|
649
|
-
kw in line.lower() for kw in ("error", "exception", "traceback", "failed", "fatal", "critical")
|
|
650
|
-
):
|
|
651
|
-
# Include surrounding context (1 line before, 2 after)
|
|
652
|
-
start = max(0, i - 1)
|
|
653
|
-
end = min(len(lines), i + 3)
|
|
654
|
-
file_errors.append({
|
|
655
|
-
"line": i + 1,
|
|
656
|
-
"context": "\n".join(lines[start:end])
|
|
657
|
-
})
|
|
658
|
-
|
|
659
|
-
if file_errors:
|
|
660
|
-
errors.append({
|
|
661
|
-
"file": log_file.name,
|
|
662
|
-
"path": str(log_file),
|
|
663
|
-
"errors": file_errors[:50] # Cap per file
|
|
664
|
-
})
|
|
665
|
-
|
|
666
|
-
return errors
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
# ── Format output as plain text ───────────────────────────────────────────
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
def format_section(title: str, data, indent: int = 0) -> str:
|
|
673
|
-
"""Format a data section as readable plain text."""
|
|
674
|
-
prefix = " " * indent
|
|
675
|
-
lines = [f"\n{'=' * 70}", f"{title}", f"{'=' * 70}"]
|
|
676
|
-
|
|
677
|
-
if isinstance(data, list):
|
|
678
|
-
if not data:
|
|
679
|
-
lines.append(f"{prefix}(none)")
|
|
680
|
-
else:
|
|
681
|
-
for i, item in enumerate(data):
|
|
682
|
-
lines.append(f"\n{prefix}--- [{i + 1}] ---")
|
|
683
|
-
if isinstance(item, dict):
|
|
684
|
-
for k, v in item.items():
|
|
685
|
-
val_str = str(v)
|
|
686
|
-
if len(val_str) > 500:
|
|
687
|
-
val_str = val_str[:500] + "..."
|
|
688
|
-
lines.append(f"{prefix} {k}: {val_str}")
|
|
689
|
-
else:
|
|
690
|
-
lines.append(f"{prefix} {item}")
|
|
691
|
-
elif isinstance(data, dict):
|
|
692
|
-
for k, v in data.items():
|
|
693
|
-
val_str = str(v)
|
|
694
|
-
if len(val_str) > 500:
|
|
695
|
-
val_str = val_str[:500] + "..."
|
|
696
|
-
lines.append(f"{prefix}{k}: {val_str}")
|
|
697
|
-
elif isinstance(data, str):
|
|
698
|
-
lines.append(data)
|
|
699
|
-
else:
|
|
700
|
-
lines.append(str(data))
|
|
701
|
-
|
|
702
|
-
return "\n".join(lines)
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
def format_transcripts(sessions: list[dict]) -> str:
|
|
706
|
-
"""Format transcripts in a readable way for Claude to analyze."""
|
|
707
|
-
lines = [f"\n{'=' * 70}", "SESSION TRANSCRIPTS", f"{'=' * 70}"]
|
|
708
|
-
lines.append(f"Total sessions: {len(sessions)}")
|
|
709
|
-
|
|
710
|
-
for i, session in enumerate(sessions):
|
|
711
|
-
lines.append(f"\n{'─' * 60}")
|
|
712
|
-
lines.append(f"SESSION {i + 1}: {session['session_file']}")
|
|
713
|
-
lines.append(f"Client: {session.get('client', 'unknown')}")
|
|
714
|
-
if session.get("source"):
|
|
715
|
-
lines.append(f"Source: {session['source']}")
|
|
716
|
-
lines.append(f"Modified: {session['modified']}")
|
|
717
|
-
lines.append(f"Messages: {session['message_count']}, Tool uses: {session['tool_use_count']}")
|
|
718
|
-
lines.append(f"{'─' * 60}")
|
|
719
|
-
|
|
720
|
-
for msg in session["messages"]:
|
|
721
|
-
role = "USER" if msg["role"] == "user" else "AGENT"
|
|
722
|
-
idx = msg.get("index", "?")
|
|
723
|
-
lines.append(f"\n[{role} @{idx}]")
|
|
724
|
-
lines.append(_redact_sensitive(msg["text"]))
|
|
725
|
-
|
|
726
|
-
if session["tool_uses"]:
|
|
727
|
-
lines.append(f"\n -- Tool usage log --")
|
|
728
|
-
for tu in session["tool_uses"]:
|
|
729
|
-
file_info = f" [{_redact_sensitive(tu['file'][:80])}]" if tu.get("file") else ""
|
|
730
|
-
lines.append(f" - {tu['tool']}{file_info}")
|
|
731
|
-
|
|
732
|
-
return "\n".join(lines)
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
# ── Main ──────────────────────────────────────────────────────────────────
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
def main():
|
|
739
|
-
# Watermark-based collection: since_iso and until_iso passed by the wrapper script
|
|
740
|
-
# argv[1] = run_id (date label for output files)
|
|
741
|
-
# argv[2] = since_iso (exclusive lower bound, e.g. "2026-04-01T04:30:00")
|
|
742
|
-
# argv[3] = until_iso (inclusive upper bound, e.g. "2026-04-02T04:30:00") — optional, defaults to now
|
|
743
|
-
run_id = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
|
|
744
|
-
since_iso = sys.argv[2] if len(sys.argv) > 2 else ""
|
|
745
|
-
until_iso = sys.argv[3] if len(sys.argv) > 3 else ""
|
|
746
|
-
|
|
747
|
-
DEEP_SLEEP_DIR.mkdir(parents=True, exist_ok=True)
|
|
748
|
-
|
|
749
|
-
print(f"[collect] Phase 1: Collecting context (run_id={run_id})")
|
|
750
|
-
|
|
751
|
-
# 1. Transcripts — watermark-based
|
|
752
|
-
if since_iso:
|
|
753
|
-
print(f"[collect] Gathering transcripts since {since_iso}" + (f" until {until_iso}" if until_iso else ""))
|
|
754
|
-
sessions = collect_transcripts_since(since_iso, until_iso)
|
|
755
|
-
else:
|
|
756
|
-
# Fallback: collect everything from last 48h (safe catch-all)
|
|
757
|
-
fallback_since = (datetime.now() - timedelta(hours=48)).isoformat()
|
|
758
|
-
print(f"[collect] No watermark — collecting last 48h since {fallback_since}")
|
|
759
|
-
sessions = collect_transcripts_since(fallback_since)
|
|
760
|
-
print(f" Found {len(sessions)} sessions")
|
|
761
|
-
|
|
762
|
-
if not sessions:
|
|
763
|
-
print(f"[collect] No new sessions found. Writing minimal context file.")
|
|
764
|
-
output_file = DEEP_SLEEP_DIR / f"{run_id}-context.txt"
|
|
765
|
-
output_file.write_text(
|
|
766
|
-
f"Deep Sleep Context for {run_id}\n\nNo sessions found.\n"
|
|
767
|
-
)
|
|
768
|
-
print(f"[collect] Output: {output_file}")
|
|
769
|
-
return
|
|
770
|
-
|
|
771
|
-
target_date = run_id # Keep variable name for downstream compat
|
|
772
|
-
|
|
773
|
-
# 2. Core DB data
|
|
774
|
-
print("[collect] Querying databases...")
|
|
775
|
-
followups = collect_followups()
|
|
776
|
-
print(f" Active followups: {len(followups)}")
|
|
777
|
-
|
|
778
|
-
learnings = collect_learnings()
|
|
779
|
-
print(f" Learnings: {len(learnings)}")
|
|
780
|
-
|
|
781
|
-
diaries = collect_diaries(target_date)
|
|
782
|
-
print(f" Diaries today: {len(diaries)}")
|
|
783
|
-
|
|
784
|
-
trust_history = collect_trust_score()
|
|
785
|
-
print(f" Trust events (7d): {len(trust_history)}")
|
|
786
|
-
|
|
787
|
-
# 3. Discovery
|
|
788
|
-
print("[collect] Scanning for non-core content...")
|
|
789
|
-
extras = discover_extras()
|
|
790
|
-
print(f" Discovered {len(extras)} extra items")
|
|
791
|
-
|
|
792
|
-
# 4. Error logs
|
|
793
|
-
print("[collect] Checking error logs...")
|
|
794
|
-
error_logs = collect_error_logs(target_date)
|
|
795
|
-
print(f" Log files with errors: {len(error_logs)}")
|
|
796
|
-
|
|
797
|
-
print("[collect] Building long-horizon context...")
|
|
798
|
-
long_horizon = collect_long_horizon_context(target_date)
|
|
799
|
-
print(
|
|
800
|
-
" Long horizon: "
|
|
801
|
-
f"{len(long_horizon.get('historical_diaries', []))} diary samples, "
|
|
802
|
-
f"{len(long_horizon.get('historical_sessions', []))} session samples"
|
|
803
|
-
)
|
|
804
|
-
|
|
805
|
-
# 5. Build per-session files + shared context
|
|
806
|
-
date_dir = DEEP_SLEEP_DIR / target_date
|
|
807
|
-
date_dir.mkdir(parents=True, exist_ok=True)
|
|
808
|
-
print(f"[collect] Writing session files to {date_dir}/")
|
|
809
|
-
|
|
810
|
-
# Shared context (followups, learnings, diaries, etc.) — one file
|
|
811
|
-
shared_parts = [
|
|
812
|
-
f"Deep Sleep Shared Context -- {target_date}",
|
|
813
|
-
f"Generated at: {datetime.now().isoformat()}",
|
|
814
|
-
f"NEXO_HOME: {NEXO_HOME}",
|
|
815
|
-
f"Sessions: {len(sessions)}",
|
|
816
|
-
]
|
|
817
|
-
shared_parts.append(format_section("ACTIVE FOLLOWUPS", followups))
|
|
818
|
-
shared_parts.append(format_section("LEARNINGS (recent 200)", learnings))
|
|
819
|
-
shared_parts.append(format_section("SESSION DIARIES TODAY", diaries))
|
|
820
|
-
shared_parts.append(format_section("TRUST SCORE HISTORY (7d)", trust_history))
|
|
821
|
-
shared_parts.append(format_section("DISCOVERED NON-CORE CONTENT", extras))
|
|
822
|
-
shared_parts.append(format_section("ERROR LOGS", error_logs))
|
|
823
|
-
shared_parts.append(format_section("LONG-HORIZON CONTEXT (60d blend)", long_horizon))
|
|
824
|
-
|
|
825
|
-
shared_text = "\n".join(shared_parts)
|
|
826
|
-
shared_file = date_dir / "shared-context.txt"
|
|
827
|
-
shared_file.write_text(shared_text, encoding="utf-8")
|
|
828
|
-
print(f" Shared context: {len(shared_text) / 1024:.0f} KB")
|
|
829
|
-
|
|
830
|
-
long_horizon_file = date_dir / "long-horizon-context.json"
|
|
831
|
-
long_horizon_file.write_text(json.dumps(long_horizon, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
832
|
-
print(f" Long horizon JSON: {long_horizon_file.name}")
|
|
833
|
-
|
|
834
|
-
# Individual session files
|
|
835
|
-
session_files_written = []
|
|
836
|
-
session_txt_map = {}
|
|
837
|
-
total_size = len(shared_text.encode("utf-8"))
|
|
838
|
-
for i, session in enumerate(sessions):
|
|
839
|
-
raw_id = session["session_file"].replace(".jsonl", "").replace(":", "-")
|
|
840
|
-
sid_short = raw_id[:30]
|
|
841
|
-
filename = f"session-{i+1:02d}-{sid_short}.txt"
|
|
842
|
-
session_path = date_dir / filename
|
|
843
|
-
|
|
844
|
-
lines = [
|
|
845
|
-
f"Session: {session['session_file']}",
|
|
846
|
-
f"Display name: {session.get('display_name', session['session_file'])}",
|
|
847
|
-
f"Client: {session.get('client', 'unknown')}",
|
|
848
|
-
f"Source: {session.get('source', 'unknown')}",
|
|
849
|
-
f"Modified: {session['modified']}",
|
|
850
|
-
f"Messages: {session['message_count']}, Tool uses: {session['tool_use_count']}",
|
|
851
|
-
f"{'─' * 60}",
|
|
852
|
-
]
|
|
853
|
-
if session.get("cwd"):
|
|
854
|
-
lines.insert(4, f"CWD: {session['cwd']}")
|
|
855
|
-
if session.get("originator"):
|
|
856
|
-
lines.insert(4, f"Originator: {session['originator']}")
|
|
857
|
-
for msg in session["messages"]:
|
|
858
|
-
role = "USER" if msg["role"] == "user" else "AGENT"
|
|
859
|
-
idx = msg.get("index", "?")
|
|
860
|
-
lines.append(f"\n[{role} @{idx}]")
|
|
861
|
-
lines.append(_redact_sensitive(msg["text"]))
|
|
862
|
-
|
|
863
|
-
if session["tool_uses"]:
|
|
864
|
-
lines.append(f"\n -- Tool usage log --")
|
|
865
|
-
for tu in session["tool_uses"]:
|
|
866
|
-
file_info = f" [{_redact_sensitive(tu['file'][:80])}]" if tu.get("file") else ""
|
|
867
|
-
lines.append(f" - {tu['tool']}{file_info}")
|
|
868
|
-
|
|
869
|
-
session_text = "\n".join(lines)
|
|
870
|
-
session_path.write_text(session_text, encoding="utf-8")
|
|
871
|
-
session_files_written.append(filename)
|
|
872
|
-
session_txt_map[session["session_file"]] = filename
|
|
873
|
-
total_size += len(session_text.encode("utf-8"))
|
|
874
|
-
print(f" {filename}: {len(session_text) / 1024:.0f} KB")
|
|
875
|
-
|
|
876
|
-
# Also keep legacy single context file for backwards compat
|
|
877
|
-
legacy_parts = [
|
|
878
|
-
f"Deep Sleep Context -- {target_date}",
|
|
879
|
-
f"Generated at: {datetime.now().isoformat()}",
|
|
880
|
-
f"NEXO_HOME: {NEXO_HOME}",
|
|
881
|
-
f"Sessions: {len(sessions)}",
|
|
882
|
-
]
|
|
883
|
-
legacy_parts.append(format_transcripts(sessions))
|
|
884
|
-
legacy_parts.append(shared_text)
|
|
885
|
-
legacy_file = DEEP_SLEEP_DIR / f"{target_date}-context.txt"
|
|
886
|
-
legacy_file.write_text("\n".join(legacy_parts), encoding="utf-8")
|
|
887
|
-
|
|
888
|
-
# Metadata JSON
|
|
889
|
-
meta = {
|
|
890
|
-
"date": target_date,
|
|
891
|
-
"sessions_found": len(sessions),
|
|
892
|
-
"session_files": [s["session_file"] for s in sessions],
|
|
893
|
-
"session_txt_files": session_files_written,
|
|
894
|
-
"session_txt_map": session_txt_map,
|
|
895
|
-
"session_manifest": [
|
|
896
|
-
{
|
|
897
|
-
"session_id": s["session_file"],
|
|
898
|
-
"display_name": s.get("display_name", s["session_file"]),
|
|
899
|
-
"client": s.get("client", "unknown"),
|
|
900
|
-
"source": s.get("source", ""),
|
|
901
|
-
"session_path": s.get("session_path", ""),
|
|
902
|
-
"session_txt_file": session_txt_map.get(s["session_file"], ""),
|
|
903
|
-
}
|
|
904
|
-
for s in sessions
|
|
905
|
-
],
|
|
906
|
-
"total_messages": sum(s["message_count"] for s in sessions),
|
|
907
|
-
"total_tool_uses": sum(s["tool_use_count"] for s in sessions),
|
|
908
|
-
"followups_active": len(followups),
|
|
909
|
-
"learnings_count": len(learnings),
|
|
910
|
-
"diaries_today": len(diaries),
|
|
911
|
-
"error_log_files": len(error_logs),
|
|
912
|
-
"date_dir": str(date_dir),
|
|
913
|
-
"shared_context_file": str(shared_file),
|
|
914
|
-
"long_horizon_file": str(long_horizon_file),
|
|
915
|
-
"context_file": str(legacy_file),
|
|
916
|
-
"total_size_bytes": total_size,
|
|
917
|
-
}
|
|
918
|
-
meta_file = DEEP_SLEEP_DIR / f"{target_date}-meta.json"
|
|
919
|
-
with open(meta_file, "w") as f:
|
|
920
|
-
json.dump(meta, f, indent=2, ensure_ascii=False)
|
|
921
|
-
|
|
922
|
-
print(f"\n[collect] Done. {len(session_files_written)} session files + shared context ({total_size / 1024:.0f} KB total)")
|
|
923
|
-
print(f"[collect] Dir: {date_dir}")
|
|
924
|
-
print(f"[collect] Meta: {meta_file}")
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
if __name__ == "__main__":
|
|
928
|
-
main()
|