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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "2.6.15",
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, and `2.6.15` hardens the installed-runtime migration path so existing users actually receive the managed bootstrap updates cleanly.
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 adapts per memory based on access patterns: frequently-accessed memories decay slower, rarely-accessed ones fade faster. Prevents permanent clutter while keeping active knowledge sharp. |
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. Surfaces non-obvious patterns like "these three bugs all relate to the same root cause." |
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. Instead of searching for "deploy error", it imagines what a helpful memory about deploy errors would look like, then searches for that. |
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. Memories retrieved together reinforce each other's connections, building an associative web that improves over time. |
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.15",
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",
@@ -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])
@@ -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=normalize_client_preferences(schedule_payload),
1449
+ preferences=normalized_preferences,
1441
1450
  )
1442
1451
  if client_sync_result.get("ok"):
1443
1452
  actions.append("client-sync")
@@ -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
- return str(json.loads(version_file.read_text()).get("operator_name", "")).strip()
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(schedule: dict | None = None) -> dict:
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 = normalize_interactive_clients(schedule.get("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"),
@@ -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
- return str(json.loads(version_file.read_text()).get("operator_name", "")).strip()
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