nexo-brain 2.6.15 → 2.6.17
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/README.md +43 -5
- package/package.json +1 -1
- package/src/agent_runner.py +70 -2
- package/src/auto_update.py +10 -1
- package/src/bootstrap_docs.py +5 -1
- package/src/client_preferences.py +68 -2
- package/src/client_sync.py +144 -2
- package/src/cognitive/__init__.py +4 -0
- package/src/cognitive/_core.py +80 -0
- package/src/cognitive/_decay.py +28 -11
- package/src/cognitive/_ingest.py +44 -22
- package/src/cognitive/_memory.py +8 -0
- package/src/cognitive/_search.py +71 -11
- package/src/dashboard/app.py +15 -8
- package/src/db/_schema.py +10 -0
- package/src/db/_sessions.py +13 -6
- package/src/doctor/providers/runtime.py +60 -5
- package/src/hooks/capture-tool-logs.sh +2 -2
- package/src/hooks/inbox-hook.sh +1 -1
- package/src/plugins/cognitive_memory.py +14 -6
- package/src/plugins/update.py +10 -1
- package/src/scripts/deep-sleep/collect.py +181 -0
- package/src/scripts/deep-sleep/synthesize-prompt.md +5 -0
- package/src/scripts/deep-sleep/synthesize.py +2 -0
- package/src/scripts/nexo-inbox-hook.sh +1 -1
- package/src/scripts/nexo-reflection.py +7 -4
- package/src/scripts/nexo-update.sh +7 -1
- package/src/server.py +13 -6
- package/src/tools_sessions.py +22 -5
- package/templates/CODEX.AGENTS.md.template +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.17",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -38,7 +38,26 @@ That means NEXO now manages not only the shared runtime and MCP wiring, but also
|
|
|
38
38
|
- For Codex specifically, `nexo chat` and Codex headless automation inject the current bootstrap explicitly, so Codex starts as NEXO even when plain global Codex startup is inconsistent about global instructions.
|
|
39
39
|
- Deep Sleep now reads both Claude Code and Codex transcript stores, so overnight analysis still works even when the user spends the day in Codex.
|
|
40
40
|
|
|
41
|
-
Version `2.6.14` closes those parity gaps in practice,
|
|
41
|
+
Version `2.6.14` closes those parity gaps in practice, `2.6.15` hardens the installed-runtime migration path so existing users actually receive the managed bootstrap updates cleanly, `2.6.16` pushes the system further in three directions, and `2.6.17` finishes the annoying last-mile migration bugs for real existing installs:
|
|
42
|
+
|
|
43
|
+
- Codex now gets managed global bootstrap/model sync in `~/.codex/config.toml`, so sessions opened outside `nexo chat` are much less likely to start as plain Codex.
|
|
44
|
+
- Retrieval is smarter by default: HyDE and spreading activation now auto-enable when the query shape benefits, while exact lookups remain conservative.
|
|
45
|
+
- Deep Sleep now blends recent context with older context over a 60-day horizon, and memory decay now tracks per-memory `stability` and `difficulty` instead of relying only on global decay constants.
|
|
46
|
+
- Existing installs that already had NEXO connected to Codex now backfill that client state automatically during update/sync, so the managed Codex bootstrap actually lands without manual cleanup.
|
|
47
|
+
- Bootstrap docs now fall back to the operator name `NEXO` when local metadata is blank, avoiding broken headings in `CLAUDE.md` and `AGENTS.md`.
|
|
48
|
+
|
|
49
|
+
### Client Capability Matrix
|
|
50
|
+
|
|
51
|
+
| Capability | Claude Code | Codex | Claude Desktop |
|
|
52
|
+
|------------|-------------|-------|----------------|
|
|
53
|
+
| Shared brain / MCP runtime | Yes | Yes | Yes |
|
|
54
|
+
| Managed bootstrap document | `~/.claude/CLAUDE.md` | `~/.codex/AGENTS.md` | Not applicable |
|
|
55
|
+
| Global startup bootstrap sync | Native via hooks + bootstrap | Managed via bootstrap + Codex config `initial_messages` | MCP only |
|
|
56
|
+
| `nexo chat` terminal client | Yes | Yes | No |
|
|
57
|
+
| Background automation backend | Recommended | Supported | No |
|
|
58
|
+
| Raw transcript source for Deep Sleep | Yes | Yes | No |
|
|
59
|
+
| Native hook depth | Deepest | Partial, compensated | None |
|
|
60
|
+
| Recommended today | Yes | Supported | Shared-brain companion |
|
|
42
61
|
|
|
43
62
|
## The Problem
|
|
44
63
|
|
|
@@ -100,12 +119,25 @@ NEXO Brain uses **Ebbinghaus forgetting curves** — memories naturally fade ove
|
|
|
100
119
|
- A lesson accessed 5 times in 2 weeks gets promoted to long-term memory — because repeated use proves it matters.
|
|
101
120
|
- A dormant memory can be reactivated if something similar comes up — the "oh wait, I remember this" moment.
|
|
102
121
|
|
|
122
|
+
On top of that baseline, NEXO now keeps a lightweight **per-memory profile**:
|
|
123
|
+
|
|
124
|
+
- **stability** slows decay for memories that keep surviving retrieval and reinforcement
|
|
125
|
+
- **difficulty** speeds decay slightly for memories that tend to be weak, noisy, or harder to reuse correctly
|
|
126
|
+
|
|
127
|
+
That keeps the core Ebbinghaus model, but makes decay more individual and less purely global.
|
|
128
|
+
|
|
103
129
|
### Semantic Search (Finding by Meaning)
|
|
104
130
|
|
|
105
131
|
NEXO Brain doesn't search by keywords. It searches by **meaning** using vector embeddings (fastembed, 768 dimensions).
|
|
106
132
|
|
|
107
133
|
Example: If you search for "deploy problems", NEXO Brain will find a memory about "SSH connection timeout on production server" — even though they share zero words. This is how human associative memory works.
|
|
108
134
|
|
|
135
|
+
Retrieval is now also smarter by default:
|
|
136
|
+
|
|
137
|
+
- **HyDE auto mode** expands conceptual or ambiguous queries when that improves recall
|
|
138
|
+
- **Spreading activation auto mode** adds a shallow associative boost for concept-heavy searches
|
|
139
|
+
- **Exact lookup heuristics** keep both off for literal file paths, IDs, stack traces, and other precision-sensitive queries
|
|
140
|
+
|
|
109
141
|
### Metacognition (Thinking About Thinking)
|
|
110
142
|
|
|
111
143
|
Before every code change, NEXO Brain asks itself: **"Have I made a mistake like this before?"**
|
|
@@ -156,6 +188,12 @@ Like a human brain, NEXO Brain has automated processes that run while you're not
|
|
|
156
188
|
|
|
157
189
|
If your Mac was asleep during any scheduled process, NEXO Brain catches up in order when it wakes.
|
|
158
190
|
|
|
191
|
+
Deep Sleep now also mixes **recent context with older context across a 60-day horizon**. Instead of only looking at the immediate past, it can surface:
|
|
192
|
+
|
|
193
|
+
- recurring multi-week themes
|
|
194
|
+
- cross-domain links between older learnings and current failures
|
|
195
|
+
- stale followups and topics that keep being mentioned but never formalized
|
|
196
|
+
|
|
159
197
|
## Cognitive Cortex
|
|
160
198
|
|
|
161
199
|
The Cortex is a middleware cognitive layer that makes the agent **think before acting**. It implements architectural inhibitory control — the agent cannot bypass reasoning.
|
|
@@ -235,21 +273,21 @@ NEXO Brain provides **150+ MCP tools** across 23 categories. These features impl
|
|
|
235
273
|
|---------|-------------|
|
|
236
274
|
| **Pin / Snooze / Archive** | Granular lifecycle states for memories. Pin = never decays (critical knowledge). Snooze = temporarily hidden (revisit later). Archive = cold storage (searchable but inactive). |
|
|
237
275
|
| **Intelligent Chunking** | Adaptive chunking that respects sentence and paragraph boundaries. Produces semantically coherent chunks instead of arbitrary token splits, reducing retrieval noise. |
|
|
238
|
-
| **Adaptive Decay** | Decay rate
|
|
276
|
+
| **Adaptive Decay** | Decay rate still follows Ebbinghaus as the base model, but now also adapts per memory using `stability` and `difficulty` profiles. Frequently reinforced memories become stickier; fragile memories fade sooner. |
|
|
239
277
|
| **Auto-Migration** | Formal schema migration system (schema_migrations table) tracks all database changes. Safe, reversible schema evolution for production systems — upgrades never lose data. |
|
|
240
278
|
| **Auto-Merge Duplicates** | Batch cosine deduplication during the 03:00 sleep cycle. Respects sibling discrimination — similar memories about different contexts are kept separate. |
|
|
241
|
-
| **Memory Dreaming** | Discovers hidden connections between recent memories during the 03:00 sleep cycle
|
|
279
|
+
| **Memory Dreaming** | Discovers hidden connections between recent memories during the 03:00 sleep cycle and now feeds a 60-day long-horizon Deep Sleep blend, so older patterns can reappear when they become relevant again. |
|
|
242
280
|
|
|
243
281
|
### Retrieval
|
|
244
282
|
|
|
245
283
|
| Feature | What It Does |
|
|
246
284
|
|---------|-------------|
|
|
247
|
-
| **HyDE Query Expansion** | Generates hypothetical answer embeddings for richer semantic search.
|
|
285
|
+
| **HyDE Query Expansion** | Generates hypothetical answer embeddings for richer semantic search. NEXO now auto-enables HyDE for conceptual or ambiguous queries while keeping literal lookups conservative. |
|
|
248
286
|
| **Hybrid Search (FTS5+BM25+RRF)** | Combines dense vector search with BM25 keyword search via Reciprocal Rank Fusion. Outperforms pure semantic search on precise terminology and code identifiers. |
|
|
249
287
|
| **Cross-Encoder Reranking** | After initial vector retrieval, a cross-encoder model rescores candidates for precision. The top-k results are reordered by true semantic relevance before being returned to the agent. |
|
|
250
288
|
| **Multi-Query Decomposition** | Complex questions are automatically split into sub-queries. Each component is retrieved independently, then fused for a higher-quality answer — improves recall on multi-faceted prompts. |
|
|
251
289
|
| **Temporal Indexing** | Memories are indexed by time in addition to semantics. Time-sensitive queries ("what did we decide last Tuesday?") use temporal proximity scoring alongside semantic similarity. |
|
|
252
|
-
| **Spreading Activation** | Graph-based co-activation network.
|
|
290
|
+
| **Spreading Activation** | Graph-based co-activation network. NEXO now auto-enables a shallow spreading pass for concept-heavy queries, improving contextual recall without turning every exact lookup into a fuzzy search. |
|
|
253
291
|
| **Recall Explanations** | Transparent score breakdown for every retrieval result. Shows exactly why a memory was returned: semantic similarity, recency, access frequency, and co-activation bonuses. |
|
|
254
292
|
|
|
255
293
|
### Proactive
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.17",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
package/src/agent_runner.py
CHANGED
|
@@ -4,9 +4,11 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import shlex
|
|
7
8
|
import shutil
|
|
8
9
|
import subprocess
|
|
9
10
|
import tempfile
|
|
11
|
+
import tomllib
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
|
|
12
14
|
from client_preferences import (
|
|
@@ -66,6 +68,10 @@ def _resolve_codex_cli() -> str:
|
|
|
66
68
|
return shutil.which("codex") or ""
|
|
67
69
|
|
|
68
70
|
|
|
71
|
+
def _codex_config_path() -> Path:
|
|
72
|
+
return Path.home() / ".codex" / "config.toml"
|
|
73
|
+
|
|
74
|
+
|
|
69
75
|
def _headless_env(env: dict | None = None) -> dict:
|
|
70
76
|
merged = os.environ.copy()
|
|
71
77
|
if env:
|
|
@@ -84,6 +90,21 @@ def _load_client_bootstrap_prompt(client: str) -> str:
|
|
|
84
90
|
return load_bootstrap_prompt(client, nexo_home=NEXO_HOME, user_home=Path.home())
|
|
85
91
|
|
|
86
92
|
|
|
93
|
+
def _codex_managed_initial_messages_enabled() -> bool:
|
|
94
|
+
config_path = _codex_config_path()
|
|
95
|
+
if not config_path.is_file():
|
|
96
|
+
return False
|
|
97
|
+
try:
|
|
98
|
+
payload = tomllib.loads(config_path.read_text())
|
|
99
|
+
except Exception:
|
|
100
|
+
return False
|
|
101
|
+
return bool(
|
|
102
|
+
payload.get("nexo", {})
|
|
103
|
+
.get("codex", {})
|
|
104
|
+
.get("bootstrap_managed")
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
87
108
|
def _codex_initial_messages_config(prompt_text: str) -> str:
|
|
88
109
|
return f'initial_messages=[{{role="system",content={json.dumps(prompt_text, ensure_ascii=False)}}}]'
|
|
89
110
|
|
|
@@ -121,7 +142,7 @@ def build_interactive_client_command(
|
|
|
121
142
|
)
|
|
122
143
|
cmd = [codex_bin]
|
|
123
144
|
bootstrap_prompt = _load_client_bootstrap_prompt(CLIENT_CODEX)
|
|
124
|
-
if bootstrap_prompt:
|
|
145
|
+
if bootstrap_prompt and not _codex_managed_initial_messages_enabled():
|
|
125
146
|
cmd.extend(["-c", _codex_initial_messages_config(bootstrap_prompt)])
|
|
126
147
|
if profile["model"]:
|
|
127
148
|
cmd.extend(["-m", profile["model"]])
|
|
@@ -147,6 +168,53 @@ def launch_interactive_client(
|
|
|
147
168
|
return subprocess.run(cmd, env=launch_env)
|
|
148
169
|
|
|
149
170
|
|
|
171
|
+
def build_followup_terminal_shell_command(
|
|
172
|
+
followup_reference: str,
|
|
173
|
+
*,
|
|
174
|
+
client: str | None = None,
|
|
175
|
+
preferences: dict | None = None,
|
|
176
|
+
cwd: str | os.PathLike[str] | None = None,
|
|
177
|
+
) -> tuple[str, str]:
|
|
178
|
+
prefs = preferences or load_client_preferences()
|
|
179
|
+
selected = resolve_terminal_client(client, preferences=prefs)
|
|
180
|
+
profile = resolve_client_runtime_profile(selected, preferences=prefs)
|
|
181
|
+
prompt = f"NEXO: execute followup from file $(cat {followup_reference})"
|
|
182
|
+
|
|
183
|
+
if selected == CLIENT_CLAUDE_CODE:
|
|
184
|
+
claude_bin = _resolve_claude_cli()
|
|
185
|
+
if not claude_bin:
|
|
186
|
+
raise TerminalClientUnavailableError(
|
|
187
|
+
"Claude Code launcher not found in PATH. Install `claude` first."
|
|
188
|
+
)
|
|
189
|
+
cmd = [claude_bin]
|
|
190
|
+
if profile["model"]:
|
|
191
|
+
cmd.extend(["--model", profile["model"]])
|
|
192
|
+
if profile["reasoning_effort"]:
|
|
193
|
+
cmd.extend(["--effort", profile["reasoning_effort"]])
|
|
194
|
+
cmd.extend(["--dangerously-skip-permissions", prompt])
|
|
195
|
+
return selected, shlex.join(cmd)
|
|
196
|
+
|
|
197
|
+
if selected == CLIENT_CODEX:
|
|
198
|
+
codex_bin = _resolve_codex_cli()
|
|
199
|
+
if not codex_bin:
|
|
200
|
+
raise TerminalClientUnavailableError(
|
|
201
|
+
"Codex launcher not found in PATH. Install `codex` first or reconfigure NEXO."
|
|
202
|
+
)
|
|
203
|
+
target_cwd = str(Path(cwd).expanduser()) if cwd else str(Path.home())
|
|
204
|
+
cmd = [codex_bin]
|
|
205
|
+
bootstrap_prompt = _load_client_bootstrap_prompt(CLIENT_CODEX)
|
|
206
|
+
if bootstrap_prompt and not _codex_managed_initial_messages_enabled():
|
|
207
|
+
cmd.extend(["-c", _codex_initial_messages_config(bootstrap_prompt)])
|
|
208
|
+
if profile["model"]:
|
|
209
|
+
cmd.extend(["-m", profile["model"]])
|
|
210
|
+
if profile["reasoning_effort"]:
|
|
211
|
+
cmd.extend(["-c", f'model_reasoning_effort="{profile["reasoning_effort"]}"'])
|
|
212
|
+
cmd.extend(["-C", target_cwd, prompt])
|
|
213
|
+
return selected, shlex.join(cmd)
|
|
214
|
+
|
|
215
|
+
raise TerminalClientUnavailableError(f"Unsupported terminal client: {selected}")
|
|
216
|
+
|
|
217
|
+
|
|
150
218
|
def _resolve_runtime_model_and_effort(
|
|
151
219
|
client: str,
|
|
152
220
|
*,
|
|
@@ -270,7 +338,7 @@ def run_automation_prompt(
|
|
|
270
338
|
str(output_path),
|
|
271
339
|
]
|
|
272
340
|
bootstrap_prompt = _load_client_bootstrap_prompt(CLIENT_CODEX)
|
|
273
|
-
if bootstrap_prompt:
|
|
341
|
+
if bootstrap_prompt and not _codex_managed_initial_messages_enabled():
|
|
274
342
|
cmd.extend(["-c", _codex_initial_messages_config(bootstrap_prompt)])
|
|
275
343
|
if resolved_model:
|
|
276
344
|
cmd.extend(["-m", resolved_model])
|
package/src/auto_update.py
CHANGED
|
@@ -1434,10 +1434,19 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
|
|
|
1434
1434
|
|
|
1435
1435
|
schedule_path = dest / "config" / "schedule.json"
|
|
1436
1436
|
schedule_payload = json.loads(schedule_path.read_text()) if schedule_path.exists() else {}
|
|
1437
|
+
normalized_preferences = normalize_client_preferences(schedule_payload)
|
|
1438
|
+
if normalized_preferences != {
|
|
1439
|
+
key: schedule_payload.get(key)
|
|
1440
|
+
for key in normalized_preferences
|
|
1441
|
+
}:
|
|
1442
|
+
merged_schedule = dict(schedule_payload)
|
|
1443
|
+
merged_schedule.update(normalized_preferences)
|
|
1444
|
+
schedule_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1445
|
+
schedule_path.write_text(json.dumps(merged_schedule, indent=2, ensure_ascii=False) + "\n")
|
|
1437
1446
|
client_sync_result = sync_all_clients(
|
|
1438
1447
|
nexo_home=dest,
|
|
1439
1448
|
runtime_root=dest,
|
|
1440
|
-
preferences=
|
|
1449
|
+
preferences=normalized_preferences,
|
|
1441
1450
|
)
|
|
1442
1451
|
if client_sync_result.get("ok"):
|
|
1443
1452
|
actions.append("client-sync")
|
package/src/bootstrap_docs.py
CHANGED
|
@@ -73,7 +73,9 @@ def _resolve_operator_name(nexo_home: Path, explicit: str = "") -> str:
|
|
|
73
73
|
version_file = nexo_home / "version.json"
|
|
74
74
|
if version_file.is_file():
|
|
75
75
|
try:
|
|
76
|
-
|
|
76
|
+
candidate = str(json.loads(version_file.read_text()).get("operator_name", "")).strip()
|
|
77
|
+
if candidate:
|
|
78
|
+
return candidate
|
|
77
79
|
except Exception:
|
|
78
80
|
pass
|
|
79
81
|
return "NEXO"
|
|
@@ -236,6 +238,7 @@ def sync_client_bootstrap(
|
|
|
236
238
|
"action": "created",
|
|
237
239
|
"path": str(target_path),
|
|
238
240
|
"version": template_version,
|
|
241
|
+
"content": rendered,
|
|
239
242
|
}
|
|
240
243
|
|
|
241
244
|
existing = target_path.read_text()
|
|
@@ -275,6 +278,7 @@ def sync_client_bootstrap(
|
|
|
275
278
|
"action": action,
|
|
276
279
|
"path": str(target_path),
|
|
277
280
|
"version": template_version,
|
|
281
|
+
"content": updated,
|
|
278
282
|
}
|
|
279
283
|
|
|
280
284
|
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
import shutil
|
|
7
7
|
import sys
|
|
8
|
+
import tomllib
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
from runtime_power import load_schedule_config, save_schedule_config
|
|
@@ -51,6 +52,14 @@ def _user_home() -> Path:
|
|
|
51
52
|
return Path(os.environ.get("HOME", str(Path.home()))).expanduser()
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
def _codex_config_path(home: Path) -> Path:
|
|
56
|
+
return home / ".codex" / "config.toml"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _codex_bootstrap_path(home: Path) -> Path:
|
|
60
|
+
return home / ".codex" / "AGENTS.md"
|
|
61
|
+
|
|
62
|
+
|
|
54
63
|
def _coerce_bool(value, default: bool) -> bool:
|
|
55
64
|
if isinstance(value, bool):
|
|
56
65
|
return value
|
|
@@ -127,6 +136,56 @@ def normalize_interactive_clients(value) -> dict[str, bool]:
|
|
|
127
136
|
return normalized
|
|
128
137
|
|
|
129
138
|
|
|
139
|
+
def _codex_artifacts_suggest_nexo_management(home: Path) -> bool:
|
|
140
|
+
bootstrap_path = _codex_bootstrap_path(home)
|
|
141
|
+
if bootstrap_path.is_file():
|
|
142
|
+
try:
|
|
143
|
+
bootstrap_text = bootstrap_path.read_text()
|
|
144
|
+
except Exception:
|
|
145
|
+
bootstrap_text = ""
|
|
146
|
+
if (
|
|
147
|
+
"nexo-codex-agents-version:" in bootstrap_text
|
|
148
|
+
or "NEXO Shared Brain for Codex" in bootstrap_text
|
|
149
|
+
or "<!-- nexo:core:start -->" in bootstrap_text
|
|
150
|
+
):
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
config_path = _codex_config_path(home)
|
|
154
|
+
if not config_path.is_file():
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
payload = tomllib.loads(config_path.read_text())
|
|
159
|
+
except Exception:
|
|
160
|
+
try:
|
|
161
|
+
raw_text = config_path.read_text()
|
|
162
|
+
except Exception:
|
|
163
|
+
return False
|
|
164
|
+
return "[mcp_servers.nexo]" in raw_text or "[nexo.codex]" in raw_text
|
|
165
|
+
|
|
166
|
+
if not isinstance(payload, dict):
|
|
167
|
+
return False
|
|
168
|
+
mcp_servers = payload.get("mcp_servers")
|
|
169
|
+
if isinstance(mcp_servers, dict) and "nexo" in mcp_servers:
|
|
170
|
+
return True
|
|
171
|
+
nexo_table = payload.get("nexo")
|
|
172
|
+
if isinstance(nexo_table, dict) and "codex" in nexo_table:
|
|
173
|
+
return True
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _backfill_interactive_clients(
|
|
178
|
+
interactive_clients: dict[str, bool],
|
|
179
|
+
*,
|
|
180
|
+
user_home: str | os.PathLike[str] | None = None,
|
|
181
|
+
) -> dict[str, bool]:
|
|
182
|
+
normalized = dict(interactive_clients)
|
|
183
|
+
home = Path(user_home).expanduser() if user_home else _user_home()
|
|
184
|
+
if not normalized.get(CLIENT_CODEX, False) and _codex_artifacts_suggest_nexo_management(home):
|
|
185
|
+
normalized[CLIENT_CODEX] = True
|
|
186
|
+
return normalized
|
|
187
|
+
|
|
188
|
+
|
|
130
189
|
def normalize_default_terminal_client(value, interactive_clients: dict[str, bool] | None = None) -> str:
|
|
131
190
|
interactive_clients = normalize_interactive_clients(interactive_clients or {})
|
|
132
191
|
candidate = normalize_client_key(value)
|
|
@@ -210,9 +269,16 @@ def normalize_client_runtime_profiles(value) -> dict[str, dict[str, str]]:
|
|
|
210
269
|
return normalized
|
|
211
270
|
|
|
212
271
|
|
|
213
|
-
def normalize_client_preferences(
|
|
272
|
+
def normalize_client_preferences(
|
|
273
|
+
schedule: dict | None = None,
|
|
274
|
+
*,
|
|
275
|
+
user_home: str | os.PathLike[str] | None = None,
|
|
276
|
+
) -> dict:
|
|
214
277
|
schedule = dict(schedule or {})
|
|
215
|
-
interactive_clients =
|
|
278
|
+
interactive_clients = _backfill_interactive_clients(
|
|
279
|
+
normalize_interactive_clients(schedule.get("interactive_clients")),
|
|
280
|
+
user_home=user_home,
|
|
281
|
+
)
|
|
216
282
|
automation_enabled = normalize_automation_enabled(schedule.get("automation_enabled"))
|
|
217
283
|
default_terminal_client = normalize_default_terminal_client(
|
|
218
284
|
schedule.get("default_terminal_client"),
|
package/src/client_sync.py
CHANGED
|
@@ -8,6 +8,7 @@ import os
|
|
|
8
8
|
import shutil
|
|
9
9
|
import subprocess
|
|
10
10
|
import sys
|
|
11
|
+
import tomllib
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
|
|
13
14
|
from bootstrap_docs import sync_client_bootstrap
|
|
@@ -19,6 +20,7 @@ try:
|
|
|
19
20
|
normalize_backend_key,
|
|
20
21
|
normalize_client_key,
|
|
21
22
|
normalize_client_preferences,
|
|
23
|
+
resolve_client_runtime_profile,
|
|
22
24
|
)
|
|
23
25
|
except Exception:
|
|
24
26
|
BACKEND_NONE = "none"
|
|
@@ -51,6 +53,13 @@ except Exception:
|
|
|
51
53
|
"automation_backend": "claude_code",
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
def resolve_client_runtime_profile(client: str, preferences: dict | None = None) -> dict:
|
|
57
|
+
defaults = {
|
|
58
|
+
"claude_code": {"model": "opus", "reasoning_effort": ""},
|
|
59
|
+
"codex": {"model": "gpt-5.4", "reasoning_effort": "xhigh"},
|
|
60
|
+
}
|
|
61
|
+
return dict(defaults.get(client, {}))
|
|
62
|
+
|
|
54
63
|
|
|
55
64
|
|
|
56
65
|
def _user_home() -> Path:
|
|
@@ -71,10 +80,12 @@ def _resolve_operator_name(nexo_home: Path, explicit: str = "") -> str:
|
|
|
71
80
|
version_file = nexo_home / "version.json"
|
|
72
81
|
if version_file.is_file():
|
|
73
82
|
try:
|
|
74
|
-
|
|
83
|
+
candidate = str(json.loads(version_file.read_text()).get("operator_name", "")).strip()
|
|
84
|
+
if candidate:
|
|
85
|
+
return candidate
|
|
75
86
|
except Exception:
|
|
76
87
|
pass
|
|
77
|
-
return ""
|
|
88
|
+
return "NEXO"
|
|
78
89
|
|
|
79
90
|
|
|
80
91
|
def _resolve_runtime_root(nexo_home: Path, runtime_root: str | os.PathLike[str] | None = None) -> Path:
|
|
@@ -156,6 +167,118 @@ def _codex_config_path(home: Path | None = None) -> Path:
|
|
|
156
167
|
return base / ".codex" / "config.toml"
|
|
157
168
|
|
|
158
169
|
|
|
170
|
+
def _toml_key(key: str) -> str:
|
|
171
|
+
if key.replace("_", "").replace("-", "").isalnum():
|
|
172
|
+
return key
|
|
173
|
+
escaped = key.replace("\\", "\\\\").replace('"', '\\"')
|
|
174
|
+
return f'"{escaped}"'
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _toml_scalar(value) -> str:
|
|
178
|
+
if isinstance(value, bool):
|
|
179
|
+
return "true" if value else "false"
|
|
180
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
181
|
+
return json.dumps(value)
|
|
182
|
+
escaped = str(value).replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
|
183
|
+
return f'"{escaped}"'
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _toml_inline_table(payload: dict) -> str:
|
|
187
|
+
parts = [f"{_toml_key(str(key))} = {_toml_value(value)}" for key, value in payload.items()]
|
|
188
|
+
return "{ " + ", ".join(parts) + " }"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _toml_value(value) -> str:
|
|
192
|
+
if isinstance(value, dict):
|
|
193
|
+
return _toml_inline_table(value)
|
|
194
|
+
if isinstance(value, list):
|
|
195
|
+
return "[" + ", ".join(_toml_value(item) for item in value) + "]"
|
|
196
|
+
return _toml_scalar(value)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _emit_toml_table(table: dict, prefix: tuple[str, ...] = ()) -> list[str]:
|
|
200
|
+
scalar_lines: list[str] = []
|
|
201
|
+
child_tables: list[tuple[str, dict]] = []
|
|
202
|
+
for key, value in table.items():
|
|
203
|
+
if isinstance(value, dict):
|
|
204
|
+
child_tables.append((str(key), value))
|
|
205
|
+
else:
|
|
206
|
+
scalar_lines.append(f"{_toml_key(str(key))} = {_toml_value(value)}")
|
|
207
|
+
|
|
208
|
+
lines: list[str] = []
|
|
209
|
+
emit_header = bool(prefix and (scalar_lines or not child_tables))
|
|
210
|
+
if emit_header:
|
|
211
|
+
lines.append("[" + ".".join(_toml_key(part) for part in prefix) + "]")
|
|
212
|
+
lines.extend(scalar_lines)
|
|
213
|
+
|
|
214
|
+
for child_key, child_value in child_tables:
|
|
215
|
+
child_lines = _emit_toml_table(child_value, prefix + (child_key,))
|
|
216
|
+
if child_lines:
|
|
217
|
+
if lines:
|
|
218
|
+
lines.append("")
|
|
219
|
+
lines.extend(child_lines)
|
|
220
|
+
return lines
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _load_toml_object(path: Path) -> dict:
|
|
224
|
+
if not path.is_file():
|
|
225
|
+
return {}
|
|
226
|
+
try:
|
|
227
|
+
data = tomllib.loads(path.read_text())
|
|
228
|
+
except Exception as exc:
|
|
229
|
+
raise ValueError(f"Invalid TOML in {path}: {exc}") from exc
|
|
230
|
+
if not isinstance(data, dict):
|
|
231
|
+
raise ValueError(f"Expected TOML table in {path}")
|
|
232
|
+
return data
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _write_toml_object(path: Path, payload: dict) -> None:
|
|
236
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
237
|
+
lines = _emit_toml_table(payload)
|
|
238
|
+
path.write_text("\n".join(lines).rstrip() + "\n")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _sync_codex_managed_config(
|
|
242
|
+
path: Path,
|
|
243
|
+
*,
|
|
244
|
+
bootstrap_prompt: str,
|
|
245
|
+
runtime_profile: dict | None,
|
|
246
|
+
) -> dict:
|
|
247
|
+
payload = _load_toml_object(path)
|
|
248
|
+
action = "updated" if payload else "created"
|
|
249
|
+
runtime_profile = dict(runtime_profile or {})
|
|
250
|
+
|
|
251
|
+
if runtime_profile.get("model"):
|
|
252
|
+
payload["model"] = runtime_profile["model"]
|
|
253
|
+
if "reasoning_effort" in runtime_profile:
|
|
254
|
+
payload["model_reasoning_effort"] = runtime_profile.get("reasoning_effort") or ""
|
|
255
|
+
|
|
256
|
+
payload["initial_messages"] = [
|
|
257
|
+
{
|
|
258
|
+
"role": "system",
|
|
259
|
+
"content": bootstrap_prompt,
|
|
260
|
+
}
|
|
261
|
+
] if bootstrap_prompt else []
|
|
262
|
+
|
|
263
|
+
nexo_table = payload.setdefault("nexo", {})
|
|
264
|
+
codex_table = nexo_table.setdefault("codex", {})
|
|
265
|
+
codex_table["bootstrap_managed"] = True
|
|
266
|
+
codex_table["bootstrap_bytes"] = len(bootstrap_prompt.encode("utf-8")) if bootstrap_prompt else 0
|
|
267
|
+
if runtime_profile.get("model"):
|
|
268
|
+
codex_table["managed_model"] = runtime_profile["model"]
|
|
269
|
+
codex_table["managed_reasoning_effort"] = runtime_profile.get("reasoning_effort", "") or ""
|
|
270
|
+
|
|
271
|
+
_write_toml_object(path, payload)
|
|
272
|
+
return {
|
|
273
|
+
"ok": True,
|
|
274
|
+
"action": action,
|
|
275
|
+
"path": str(path),
|
|
276
|
+
"bootstrap_managed": True,
|
|
277
|
+
"model": runtime_profile.get("model", ""),
|
|
278
|
+
"reasoning_effort": runtime_profile.get("reasoning_effort", "") or "",
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
159
282
|
def _load_json_object(path: Path) -> dict:
|
|
160
283
|
if not path.is_file():
|
|
161
284
|
return {}
|
|
@@ -197,6 +320,7 @@ def sync_claude_code(
|
|
|
197
320
|
python_path: str = "",
|
|
198
321
|
operator_name: str = "",
|
|
199
322
|
user_home: str | os.PathLike[str] | None = None,
|
|
323
|
+
preferences: dict | None = None,
|
|
200
324
|
) -> dict:
|
|
201
325
|
server_config = build_server_config(
|
|
202
326
|
nexo_home=nexo_home,
|
|
@@ -229,6 +353,7 @@ def sync_claude_desktop(
|
|
|
229
353
|
python_path: str = "",
|
|
230
354
|
operator_name: str = "",
|
|
231
355
|
user_home: str | os.PathLike[str] | None = None,
|
|
356
|
+
preferences: dict | None = None,
|
|
232
357
|
) -> dict:
|
|
233
358
|
server_config = build_server_config(
|
|
234
359
|
nexo_home=nexo_home,
|
|
@@ -250,9 +375,12 @@ def sync_codex(
|
|
|
250
375
|
python_path: str = "",
|
|
251
376
|
operator_name: str = "",
|
|
252
377
|
user_home: str | os.PathLike[str] | None = None,
|
|
378
|
+
preferences: dict | None = None,
|
|
253
379
|
) -> dict:
|
|
254
380
|
nexo_home_path = Path(nexo_home).expanduser() if nexo_home else _default_nexo_home()
|
|
255
381
|
home_path = Path(user_home).expanduser() if user_home else _user_home()
|
|
382
|
+
active_preferences = normalize_client_preferences(preferences)
|
|
383
|
+
runtime_profile = resolve_client_runtime_profile("codex", preferences=active_preferences)
|
|
256
384
|
server_config = build_server_config(
|
|
257
385
|
nexo_home=nexo_home_path,
|
|
258
386
|
runtime_root=runtime_root,
|
|
@@ -276,6 +404,13 @@ def sync_codex(
|
|
|
276
404
|
user_home=user_home,
|
|
277
405
|
)
|
|
278
406
|
result["bootstrap"] = bootstrap_result
|
|
407
|
+
if bootstrap_result.get("ok"):
|
|
408
|
+
prompt_text = bootstrap_result.get("content") or ""
|
|
409
|
+
result["config"] = _sync_codex_managed_config(
|
|
410
|
+
config_path,
|
|
411
|
+
bootstrap_prompt=prompt_text,
|
|
412
|
+
runtime_profile=runtime_profile,
|
|
413
|
+
)
|
|
279
414
|
return result
|
|
280
415
|
|
|
281
416
|
cmd = [codex_bin, "mcp", "add", "nexo"]
|
|
@@ -324,6 +459,12 @@ def sync_codex(
|
|
|
324
459
|
if not bootstrap_result.get("ok"):
|
|
325
460
|
sync_result["ok"] = False
|
|
326
461
|
sync_result["error"] = bootstrap_result.get("error", "Codex bootstrap sync failed")
|
|
462
|
+
return sync_result
|
|
463
|
+
sync_result["config"] = _sync_codex_managed_config(
|
|
464
|
+
config_path,
|
|
465
|
+
bootstrap_prompt=bootstrap_result.get("content") or "",
|
|
466
|
+
runtime_profile=runtime_profile,
|
|
467
|
+
)
|
|
327
468
|
return sync_result
|
|
328
469
|
|
|
329
470
|
|
|
@@ -372,6 +513,7 @@ def sync_all_clients(
|
|
|
372
513
|
python_path=python_path,
|
|
373
514
|
operator_name=operator_name,
|
|
374
515
|
user_home=user_home,
|
|
516
|
+
preferences=preferences,
|
|
375
517
|
)
|
|
376
518
|
except Exception as exc:
|
|
377
519
|
return {"ok": False, "client": label, "error": str(exc)}
|
|
@@ -10,14 +10,18 @@ constants are re-exported here for full backwards compatibility:
|
|
|
10
10
|
# Core: DB, embedding, cosine, constants, tables, redaction
|
|
11
11
|
from cognitive._core import (
|
|
12
12
|
COGNITIVE_DB, EMBEDDING_DIM, LAMBDA_STM, LAMBDA_LTM,
|
|
13
|
+
DEFAULT_MEMORY_STABILITY, DEFAULT_MEMORY_DIFFICULTY,
|
|
13
14
|
PE_GATE_REJECT, PE_GATE_REFINE, _gate_stats,
|
|
14
15
|
DISCRIMINATING_ENTITIES,
|
|
15
16
|
POSITIVE_SIGNALS, NEGATIVE_SIGNALS, URGENCY_SIGNALS,
|
|
16
17
|
_get_db, _init_tables, _migrate_lifecycle, _migrate_co_activation,
|
|
18
|
+
_migrate_memory_personalization,
|
|
17
19
|
_auto_migrate_embeddings,
|
|
18
20
|
_get_model, _get_reranker, rerank_results,
|
|
19
21
|
embed, cosine_similarity, _array_to_blob, _blob_to_array,
|
|
20
22
|
extract_temporal_date, redact_secrets,
|
|
23
|
+
clamp_memory_stability, clamp_memory_difficulty,
|
|
24
|
+
initial_memory_profile, personalize_decay_rate, rehearsal_profile_update,
|
|
21
25
|
)
|
|
22
26
|
|
|
23
27
|
# Search
|