nexo-brain 2.6.15 → 2.6.16
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 +41 -5
- package/package.json +1 -1
- package/src/agent_runner.py +70 -2
- package/src/bootstrap_docs.py +2 -0
- package/src/client_sync.py +140 -0
- 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/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/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.16",
|
|
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,24 @@ 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, and `2.6.16` pushes the system further in three directions:
|
|
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
|
+
|
|
47
|
+
### Client Capability Matrix
|
|
48
|
+
|
|
49
|
+
| Capability | Claude Code | Codex | Claude Desktop |
|
|
50
|
+
|------------|-------------|-------|----------------|
|
|
51
|
+
| Shared brain / MCP runtime | Yes | Yes | Yes |
|
|
52
|
+
| Managed bootstrap document | `~/.claude/CLAUDE.md` | `~/.codex/AGENTS.md` | Not applicable |
|
|
53
|
+
| Global startup bootstrap sync | Native via hooks + bootstrap | Managed via bootstrap + Codex config `initial_messages` | MCP only |
|
|
54
|
+
| `nexo chat` terminal client | Yes | Yes | No |
|
|
55
|
+
| Background automation backend | Recommended | Supported | No |
|
|
56
|
+
| Raw transcript source for Deep Sleep | Yes | Yes | No |
|
|
57
|
+
| Native hook depth | Deepest | Partial, compensated | None |
|
|
58
|
+
| Recommended today | Yes | Supported | Shared-brain companion |
|
|
42
59
|
|
|
43
60
|
## The Problem
|
|
44
61
|
|
|
@@ -100,12 +117,25 @@ NEXO Brain uses **Ebbinghaus forgetting curves** — memories naturally fade ove
|
|
|
100
117
|
- A lesson accessed 5 times in 2 weeks gets promoted to long-term memory — because repeated use proves it matters.
|
|
101
118
|
- A dormant memory can be reactivated if something similar comes up — the "oh wait, I remember this" moment.
|
|
102
119
|
|
|
120
|
+
On top of that baseline, NEXO now keeps a lightweight **per-memory profile**:
|
|
121
|
+
|
|
122
|
+
- **stability** slows decay for memories that keep surviving retrieval and reinforcement
|
|
123
|
+
- **difficulty** speeds decay slightly for memories that tend to be weak, noisy, or harder to reuse correctly
|
|
124
|
+
|
|
125
|
+
That keeps the core Ebbinghaus model, but makes decay more individual and less purely global.
|
|
126
|
+
|
|
103
127
|
### Semantic Search (Finding by Meaning)
|
|
104
128
|
|
|
105
129
|
NEXO Brain doesn't search by keywords. It searches by **meaning** using vector embeddings (fastembed, 768 dimensions).
|
|
106
130
|
|
|
107
131
|
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
132
|
|
|
133
|
+
Retrieval is now also smarter by default:
|
|
134
|
+
|
|
135
|
+
- **HyDE auto mode** expands conceptual or ambiguous queries when that improves recall
|
|
136
|
+
- **Spreading activation auto mode** adds a shallow associative boost for concept-heavy searches
|
|
137
|
+
- **Exact lookup heuristics** keep both off for literal file paths, IDs, stack traces, and other precision-sensitive queries
|
|
138
|
+
|
|
109
139
|
### Metacognition (Thinking About Thinking)
|
|
110
140
|
|
|
111
141
|
Before every code change, NEXO Brain asks itself: **"Have I made a mistake like this before?"**
|
|
@@ -156,6 +186,12 @@ Like a human brain, NEXO Brain has automated processes that run while you're not
|
|
|
156
186
|
|
|
157
187
|
If your Mac was asleep during any scheduled process, NEXO Brain catches up in order when it wakes.
|
|
158
188
|
|
|
189
|
+
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:
|
|
190
|
+
|
|
191
|
+
- recurring multi-week themes
|
|
192
|
+
- cross-domain links between older learnings and current failures
|
|
193
|
+
- stale followups and topics that keep being mentioned but never formalized
|
|
194
|
+
|
|
159
195
|
## Cognitive Cortex
|
|
160
196
|
|
|
161
197
|
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 +271,21 @@ NEXO Brain provides **150+ MCP tools** across 23 categories. These features impl
|
|
|
235
271
|
|---------|-------------|
|
|
236
272
|
| **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
273
|
| **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
|
|
274
|
+
| **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
275
|
| **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
276
|
| **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
|
|
277
|
+
| **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
278
|
|
|
243
279
|
### Retrieval
|
|
244
280
|
|
|
245
281
|
| Feature | What It Does |
|
|
246
282
|
|---------|-------------|
|
|
247
|
-
| **HyDE Query Expansion** | Generates hypothetical answer embeddings for richer semantic search.
|
|
283
|
+
| **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
284
|
| **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
285
|
| **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
286
|
| **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
287
|
| **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.
|
|
288
|
+
| **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
289
|
| **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
290
|
|
|
255
291
|
### Proactive
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.16",
|
|
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/bootstrap_docs.py
CHANGED
|
@@ -236,6 +236,7 @@ def sync_client_bootstrap(
|
|
|
236
236
|
"action": "created",
|
|
237
237
|
"path": str(target_path),
|
|
238
238
|
"version": template_version,
|
|
239
|
+
"content": rendered,
|
|
239
240
|
}
|
|
240
241
|
|
|
241
242
|
existing = target_path.read_text()
|
|
@@ -275,6 +276,7 @@ def sync_client_bootstrap(
|
|
|
275
276
|
"action": action,
|
|
276
277
|
"path": str(target_path),
|
|
277
278
|
"version": template_version,
|
|
279
|
+
"content": updated,
|
|
278
280
|
}
|
|
279
281
|
|
|
280
282
|
|
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:
|
|
@@ -156,6 +165,118 @@ def _codex_config_path(home: Path | None = None) -> Path:
|
|
|
156
165
|
return base / ".codex" / "config.toml"
|
|
157
166
|
|
|
158
167
|
|
|
168
|
+
def _toml_key(key: str) -> str:
|
|
169
|
+
if key.replace("_", "").replace("-", "").isalnum():
|
|
170
|
+
return key
|
|
171
|
+
escaped = key.replace("\\", "\\\\").replace('"', '\\"')
|
|
172
|
+
return f'"{escaped}"'
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _toml_scalar(value) -> str:
|
|
176
|
+
if isinstance(value, bool):
|
|
177
|
+
return "true" if value else "false"
|
|
178
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
179
|
+
return json.dumps(value)
|
|
180
|
+
escaped = str(value).replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
|
181
|
+
return f'"{escaped}"'
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _toml_inline_table(payload: dict) -> str:
|
|
185
|
+
parts = [f"{_toml_key(str(key))} = {_toml_value(value)}" for key, value in payload.items()]
|
|
186
|
+
return "{ " + ", ".join(parts) + " }"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _toml_value(value) -> str:
|
|
190
|
+
if isinstance(value, dict):
|
|
191
|
+
return _toml_inline_table(value)
|
|
192
|
+
if isinstance(value, list):
|
|
193
|
+
return "[" + ", ".join(_toml_value(item) for item in value) + "]"
|
|
194
|
+
return _toml_scalar(value)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _emit_toml_table(table: dict, prefix: tuple[str, ...] = ()) -> list[str]:
|
|
198
|
+
scalar_lines: list[str] = []
|
|
199
|
+
child_tables: list[tuple[str, dict]] = []
|
|
200
|
+
for key, value in table.items():
|
|
201
|
+
if isinstance(value, dict):
|
|
202
|
+
child_tables.append((str(key), value))
|
|
203
|
+
else:
|
|
204
|
+
scalar_lines.append(f"{_toml_key(str(key))} = {_toml_value(value)}")
|
|
205
|
+
|
|
206
|
+
lines: list[str] = []
|
|
207
|
+
emit_header = bool(prefix and (scalar_lines or not child_tables))
|
|
208
|
+
if emit_header:
|
|
209
|
+
lines.append("[" + ".".join(_toml_key(part) for part in prefix) + "]")
|
|
210
|
+
lines.extend(scalar_lines)
|
|
211
|
+
|
|
212
|
+
for child_key, child_value in child_tables:
|
|
213
|
+
child_lines = _emit_toml_table(child_value, prefix + (child_key,))
|
|
214
|
+
if child_lines:
|
|
215
|
+
if lines:
|
|
216
|
+
lines.append("")
|
|
217
|
+
lines.extend(child_lines)
|
|
218
|
+
return lines
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _load_toml_object(path: Path) -> dict:
|
|
222
|
+
if not path.is_file():
|
|
223
|
+
return {}
|
|
224
|
+
try:
|
|
225
|
+
data = tomllib.loads(path.read_text())
|
|
226
|
+
except Exception as exc:
|
|
227
|
+
raise ValueError(f"Invalid TOML in {path}: {exc}") from exc
|
|
228
|
+
if not isinstance(data, dict):
|
|
229
|
+
raise ValueError(f"Expected TOML table in {path}")
|
|
230
|
+
return data
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _write_toml_object(path: Path, payload: dict) -> None:
|
|
234
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
235
|
+
lines = _emit_toml_table(payload)
|
|
236
|
+
path.write_text("\n".join(lines).rstrip() + "\n")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _sync_codex_managed_config(
|
|
240
|
+
path: Path,
|
|
241
|
+
*,
|
|
242
|
+
bootstrap_prompt: str,
|
|
243
|
+
runtime_profile: dict | None,
|
|
244
|
+
) -> dict:
|
|
245
|
+
payload = _load_toml_object(path)
|
|
246
|
+
action = "updated" if payload else "created"
|
|
247
|
+
runtime_profile = dict(runtime_profile or {})
|
|
248
|
+
|
|
249
|
+
if runtime_profile.get("model"):
|
|
250
|
+
payload["model"] = runtime_profile["model"]
|
|
251
|
+
if "reasoning_effort" in runtime_profile:
|
|
252
|
+
payload["model_reasoning_effort"] = runtime_profile.get("reasoning_effort") or ""
|
|
253
|
+
|
|
254
|
+
payload["initial_messages"] = [
|
|
255
|
+
{
|
|
256
|
+
"role": "system",
|
|
257
|
+
"content": bootstrap_prompt,
|
|
258
|
+
}
|
|
259
|
+
] if bootstrap_prompt else []
|
|
260
|
+
|
|
261
|
+
nexo_table = payload.setdefault("nexo", {})
|
|
262
|
+
codex_table = nexo_table.setdefault("codex", {})
|
|
263
|
+
codex_table["bootstrap_managed"] = True
|
|
264
|
+
codex_table["bootstrap_bytes"] = len(bootstrap_prompt.encode("utf-8")) if bootstrap_prompt else 0
|
|
265
|
+
if runtime_profile.get("model"):
|
|
266
|
+
codex_table["managed_model"] = runtime_profile["model"]
|
|
267
|
+
codex_table["managed_reasoning_effort"] = runtime_profile.get("reasoning_effort", "") or ""
|
|
268
|
+
|
|
269
|
+
_write_toml_object(path, payload)
|
|
270
|
+
return {
|
|
271
|
+
"ok": True,
|
|
272
|
+
"action": action,
|
|
273
|
+
"path": str(path),
|
|
274
|
+
"bootstrap_managed": True,
|
|
275
|
+
"model": runtime_profile.get("model", ""),
|
|
276
|
+
"reasoning_effort": runtime_profile.get("reasoning_effort", "") or "",
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
159
280
|
def _load_json_object(path: Path) -> dict:
|
|
160
281
|
if not path.is_file():
|
|
161
282
|
return {}
|
|
@@ -197,6 +318,7 @@ def sync_claude_code(
|
|
|
197
318
|
python_path: str = "",
|
|
198
319
|
operator_name: str = "",
|
|
199
320
|
user_home: str | os.PathLike[str] | None = None,
|
|
321
|
+
preferences: dict | None = None,
|
|
200
322
|
) -> dict:
|
|
201
323
|
server_config = build_server_config(
|
|
202
324
|
nexo_home=nexo_home,
|
|
@@ -229,6 +351,7 @@ def sync_claude_desktop(
|
|
|
229
351
|
python_path: str = "",
|
|
230
352
|
operator_name: str = "",
|
|
231
353
|
user_home: str | os.PathLike[str] | None = None,
|
|
354
|
+
preferences: dict | None = None,
|
|
232
355
|
) -> dict:
|
|
233
356
|
server_config = build_server_config(
|
|
234
357
|
nexo_home=nexo_home,
|
|
@@ -250,9 +373,12 @@ def sync_codex(
|
|
|
250
373
|
python_path: str = "",
|
|
251
374
|
operator_name: str = "",
|
|
252
375
|
user_home: str | os.PathLike[str] | None = None,
|
|
376
|
+
preferences: dict | None = None,
|
|
253
377
|
) -> dict:
|
|
254
378
|
nexo_home_path = Path(nexo_home).expanduser() if nexo_home else _default_nexo_home()
|
|
255
379
|
home_path = Path(user_home).expanduser() if user_home else _user_home()
|
|
380
|
+
active_preferences = normalize_client_preferences(preferences)
|
|
381
|
+
runtime_profile = resolve_client_runtime_profile("codex", preferences=active_preferences)
|
|
256
382
|
server_config = build_server_config(
|
|
257
383
|
nexo_home=nexo_home_path,
|
|
258
384
|
runtime_root=runtime_root,
|
|
@@ -276,6 +402,13 @@ def sync_codex(
|
|
|
276
402
|
user_home=user_home,
|
|
277
403
|
)
|
|
278
404
|
result["bootstrap"] = bootstrap_result
|
|
405
|
+
if bootstrap_result.get("ok"):
|
|
406
|
+
prompt_text = bootstrap_result.get("content") or ""
|
|
407
|
+
result["config"] = _sync_codex_managed_config(
|
|
408
|
+
config_path,
|
|
409
|
+
bootstrap_prompt=prompt_text,
|
|
410
|
+
runtime_profile=runtime_profile,
|
|
411
|
+
)
|
|
279
412
|
return result
|
|
280
413
|
|
|
281
414
|
cmd = [codex_bin, "mcp", "add", "nexo"]
|
|
@@ -324,6 +457,12 @@ def sync_codex(
|
|
|
324
457
|
if not bootstrap_result.get("ok"):
|
|
325
458
|
sync_result["ok"] = False
|
|
326
459
|
sync_result["error"] = bootstrap_result.get("error", "Codex bootstrap sync failed")
|
|
460
|
+
return sync_result
|
|
461
|
+
sync_result["config"] = _sync_codex_managed_config(
|
|
462
|
+
config_path,
|
|
463
|
+
bootstrap_prompt=bootstrap_result.get("content") or "",
|
|
464
|
+
runtime_profile=runtime_profile,
|
|
465
|
+
)
|
|
327
466
|
return sync_result
|
|
328
467
|
|
|
329
468
|
|
|
@@ -372,6 +511,7 @@ def sync_all_clients(
|
|
|
372
511
|
python_path=python_path,
|
|
373
512
|
operator_name=operator_name,
|
|
374
513
|
user_home=user_home,
|
|
514
|
+
preferences=preferences,
|
|
375
515
|
)
|
|
376
516
|
except Exception as exc:
|
|
377
517
|
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
|
package/src/cognitive/_core.py
CHANGED
|
@@ -19,6 +19,8 @@ COGNITIVE_DB = os.path.join(_data_dir, "cognitive.db")
|
|
|
19
19
|
EMBEDDING_DIM = 768
|
|
20
20
|
LAMBDA_STM = 0.004126 # half-life = ln(2) / (7 * 24) ≈ 7 days
|
|
21
21
|
LAMBDA_LTM = 0.000481 # half-life = ln(2) / (60 * 24) ≈ 60 days
|
|
22
|
+
DEFAULT_MEMORY_STABILITY = 1.0
|
|
23
|
+
DEFAULT_MEMORY_DIFFICULTY = 0.5
|
|
22
24
|
|
|
23
25
|
# Prediction Error Gate thresholds
|
|
24
26
|
PE_GATE_REJECT = 0.85 # similarity > this → reject (not novel enough)
|
|
@@ -145,6 +147,7 @@ def _get_db() -> sqlite3.Connection:
|
|
|
145
147
|
_init_tables(_conn)
|
|
146
148
|
_migrate_lifecycle(_conn)
|
|
147
149
|
_migrate_co_activation(_conn)
|
|
150
|
+
_migrate_memory_personalization(_conn)
|
|
148
151
|
_auto_migrate_embeddings(_conn)
|
|
149
152
|
return _conn
|
|
150
153
|
|
|
@@ -192,6 +195,79 @@ def _migrate_co_activation(conn: sqlite3.Connection):
|
|
|
192
195
|
conn.commit()
|
|
193
196
|
|
|
194
197
|
|
|
198
|
+
def clamp_memory_stability(value: float | int | str | None) -> float:
|
|
199
|
+
try:
|
|
200
|
+
numeric = float(value)
|
|
201
|
+
except (TypeError, ValueError):
|
|
202
|
+
numeric = DEFAULT_MEMORY_STABILITY
|
|
203
|
+
return max(0.6, min(3.0, numeric))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def clamp_memory_difficulty(value: float | int | str | None) -> float:
|
|
207
|
+
try:
|
|
208
|
+
numeric = float(value)
|
|
209
|
+
except (TypeError, ValueError):
|
|
210
|
+
numeric = DEFAULT_MEMORY_DIFFICULTY
|
|
211
|
+
return max(0.2, min(1.2, numeric))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def initial_memory_profile(source_type: str, *, store: str = "stm") -> tuple[float, float]:
|
|
215
|
+
source = str(source_type or "").strip().lower()
|
|
216
|
+
if source in {"learning", "decision", "feedback"}:
|
|
217
|
+
return 1.2 if store == "stm" else 1.4, 0.4
|
|
218
|
+
if source in {"dream_insight", "session_summary"}:
|
|
219
|
+
return 1.1 if store == "stm" else 1.25, 0.55
|
|
220
|
+
if source in {"sensory", "dialog"}:
|
|
221
|
+
return 0.9, 0.6
|
|
222
|
+
return DEFAULT_MEMORY_STABILITY, DEFAULT_MEMORY_DIFFICULTY
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def personalize_decay_rate(base_lambda: float, *, stability: float, difficulty: float) -> float:
|
|
226
|
+
stability_factor = clamp_memory_stability(stability)
|
|
227
|
+
difficulty_factor = 0.75 + (clamp_memory_difficulty(difficulty) * 0.5)
|
|
228
|
+
return base_lambda * difficulty_factor / stability_factor
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def rehearsal_profile_update(
|
|
232
|
+
stability: float,
|
|
233
|
+
difficulty: float,
|
|
234
|
+
score: float,
|
|
235
|
+
*,
|
|
236
|
+
refinement: bool = False,
|
|
237
|
+
) -> tuple[float, float]:
|
|
238
|
+
stable = clamp_memory_stability(stability)
|
|
239
|
+
hard = clamp_memory_difficulty(difficulty)
|
|
240
|
+
score = max(0.0, min(1.0, float(score or 0.0)))
|
|
241
|
+
|
|
242
|
+
stability_gain = 0.03 + max(0.0, score - 0.45) * 0.12
|
|
243
|
+
if refinement:
|
|
244
|
+
stability_gain += 0.03
|
|
245
|
+
new_stability = clamp_memory_stability(stable + stability_gain)
|
|
246
|
+
|
|
247
|
+
target_difficulty = clamp_memory_difficulty(1.0 - (score * 0.8))
|
|
248
|
+
if refinement:
|
|
249
|
+
target_difficulty = clamp_memory_difficulty(target_difficulty + 0.05)
|
|
250
|
+
new_difficulty = clamp_memory_difficulty((hard * 0.82) + (target_difficulty * 0.18))
|
|
251
|
+
return new_stability, new_difficulty
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _migrate_memory_personalization(conn: sqlite3.Connection):
|
|
255
|
+
"""Add per-memory stability and difficulty columns if they don't exist."""
|
|
256
|
+
for table in ("stm_memories", "ltm_memories"):
|
|
257
|
+
for col, col_type in [
|
|
258
|
+
("stability", f"REAL DEFAULT {DEFAULT_MEMORY_STABILITY}"),
|
|
259
|
+
("difficulty", f"REAL DEFAULT {DEFAULT_MEMORY_DIFFICULTY}"),
|
|
260
|
+
]:
|
|
261
|
+
try:
|
|
262
|
+
conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {col_type}")
|
|
263
|
+
conn.commit()
|
|
264
|
+
except sqlite3.OperationalError as e:
|
|
265
|
+
if "duplicate column" in str(e).lower():
|
|
266
|
+
pass
|
|
267
|
+
else:
|
|
268
|
+
raise
|
|
269
|
+
|
|
270
|
+
|
|
195
271
|
def _auto_migrate_embeddings(conn: sqlite3.Connection):
|
|
196
272
|
"""Auto-detect old 384-dim embeddings and re-embed to 768-dim. Transparent to user."""
|
|
197
273
|
try:
|
|
@@ -242,6 +318,8 @@ def _init_tables(conn: sqlite3.Connection):
|
|
|
242
318
|
last_accessed TEXT DEFAULT (datetime('now')),
|
|
243
319
|
access_count INTEGER DEFAULT 0,
|
|
244
320
|
strength REAL DEFAULT 1.0,
|
|
321
|
+
stability REAL DEFAULT 1.0,
|
|
322
|
+
difficulty REAL DEFAULT 0.5,
|
|
245
323
|
promoted_to_ltm INTEGER DEFAULT 0
|
|
246
324
|
);
|
|
247
325
|
|
|
@@ -257,6 +335,8 @@ def _init_tables(conn: sqlite3.Connection):
|
|
|
257
335
|
last_accessed TEXT DEFAULT (datetime('now')),
|
|
258
336
|
access_count INTEGER DEFAULT 0,
|
|
259
337
|
strength REAL DEFAULT 1.0,
|
|
338
|
+
stability REAL DEFAULT 1.0,
|
|
339
|
+
difficulty REAL DEFAULT 0.5,
|
|
260
340
|
is_dormant INTEGER DEFAULT 0,
|
|
261
341
|
original_stm_id INTEGER,
|
|
262
342
|
tags TEXT DEFAULT ''
|
package/src/cognitive/_decay.py
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
import math
|
|
3
3
|
import numpy as np
|
|
4
4
|
from datetime import datetime, timedelta
|
|
5
|
-
from cognitive._core import
|
|
5
|
+
from cognitive._core import (
|
|
6
|
+
_get_db, embed, cosine_similarity, _blob_to_array, _array_to_blob,
|
|
7
|
+
LAMBDA_STM, LAMBDA_LTM, EMBEDDING_DIM,
|
|
8
|
+
initial_memory_profile, personalize_decay_rate,
|
|
9
|
+
)
|
|
6
10
|
|
|
7
11
|
|
|
8
12
|
def _hnsw_invalidate():
|
|
@@ -48,20 +52,32 @@ def apply_decay(adaptive: bool = True):
|
|
|
48
52
|
_protected_ltm.add(row["id"])
|
|
49
53
|
|
|
50
54
|
# STM decay (skip pinned)
|
|
51
|
-
rows = db.execute("SELECT id, last_accessed, strength FROM stm_memories WHERE promoted_to_ltm = 0 AND (lifecycle_state IS NULL OR lifecycle_state != 'pinned')").fetchall()
|
|
55
|
+
rows = db.execute("SELECT id, last_accessed, strength, stability, difficulty FROM stm_memories WHERE promoted_to_ltm = 0 AND (lifecycle_state IS NULL OR lifecycle_state != 'pinned')").fetchall()
|
|
52
56
|
for row in rows:
|
|
53
57
|
last = datetime.fromisoformat(row["last_accessed"])
|
|
54
58
|
hours = (now - last).total_seconds() / 3600.0
|
|
55
|
-
decay_rate =
|
|
59
|
+
decay_rate = personalize_decay_rate(
|
|
60
|
+
LAMBDA_STM,
|
|
61
|
+
stability=row["stability"],
|
|
62
|
+
difficulty=row["difficulty"],
|
|
63
|
+
)
|
|
64
|
+
if adaptive and row["id"] in _protected_stm:
|
|
65
|
+
decay_rate *= 0.25
|
|
56
66
|
new_strength = row["strength"] * math.exp(-decay_rate * hours)
|
|
57
67
|
db.execute("UPDATE stm_memories SET strength = ? WHERE id = ?", (new_strength, row["id"]))
|
|
58
68
|
|
|
59
69
|
# LTM decay (skip pinned)
|
|
60
|
-
rows = db.execute("SELECT id, last_accessed, strength FROM ltm_memories WHERE is_dormant = 0 AND (lifecycle_state IS NULL OR lifecycle_state != 'pinned')").fetchall()
|
|
70
|
+
rows = db.execute("SELECT id, last_accessed, strength, stability, difficulty FROM ltm_memories WHERE is_dormant = 0 AND (lifecycle_state IS NULL OR lifecycle_state != 'pinned')").fetchall()
|
|
61
71
|
for row in rows:
|
|
62
72
|
last = datetime.fromisoformat(row["last_accessed"])
|
|
63
73
|
hours = (now - last).total_seconds() / 3600.0
|
|
64
|
-
decay_rate =
|
|
74
|
+
decay_rate = personalize_decay_rate(
|
|
75
|
+
LAMBDA_LTM,
|
|
76
|
+
stability=row["stability"],
|
|
77
|
+
difficulty=row["difficulty"],
|
|
78
|
+
)
|
|
79
|
+
if adaptive and row["id"] in _protected_ltm:
|
|
80
|
+
decay_rate *= 0.25
|
|
65
81
|
new_strength = row["strength"] * math.exp(-decay_rate * hours)
|
|
66
82
|
if new_strength < 0.1:
|
|
67
83
|
db.execute("UPDATE ltm_memories SET strength = ?, is_dormant = 1 WHERE id = ?", (new_strength, row["id"]))
|
|
@@ -101,10 +117,10 @@ def promote_stm_to_ltm():
|
|
|
101
117
|
for row in rows:
|
|
102
118
|
redacted = row["redaction_applied"] if "redaction_applied" in row.keys() else 0
|
|
103
119
|
db.execute(
|
|
104
|
-
"""INSERT INTO ltm_memories (content, embedding, source_type, source_id, source_title, domain, original_stm_id, redaction_applied)
|
|
105
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
120
|
+
"""INSERT INTO ltm_memories (content, embedding, source_type, source_id, source_title, domain, original_stm_id, redaction_applied, stability, difficulty)
|
|
121
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
106
122
|
(row["content"], row["embedding"], row["source_type"], row["source_id"],
|
|
107
|
-
row["source_title"], row["domain"], row["id"], redacted)
|
|
123
|
+
row["source_title"], row["domain"], row["id"], redacted, row["stability"], row["difficulty"])
|
|
108
124
|
)
|
|
109
125
|
db.execute("UPDATE stm_memories SET promoted_to_ltm = 1 WHERE id = ?", (row["id"],))
|
|
110
126
|
promoted += 1
|
|
@@ -322,12 +338,13 @@ def dream_cycle(max_insights: int = 50) -> dict:
|
|
|
322
338
|
|
|
323
339
|
# Store as LTM with dream_insight tag
|
|
324
340
|
cur = db.execute(
|
|
325
|
-
"""INSERT INTO ltm_memories (content, embedding, source_type, source_id, source_title, domain, tags, strength)
|
|
326
|
-
VALUES (?, ?, 'dream_insight', ?, ?, ?, 'dream_insight', 0.5)""",
|
|
341
|
+
"""INSERT INTO ltm_memories (content, embedding, source_type, source_id, source_title, domain, tags, strength, stability, difficulty)
|
|
342
|
+
VALUES (?, ?, 'dream_insight', ?, ?, ?, 'dream_insight', 0.5, ?, ?)""",
|
|
327
343
|
(insight_content, blob,
|
|
328
344
|
f"{mem_a['store']}:{mem_a['id']},{mem_b['store']}:{mem_b['id']}",
|
|
329
345
|
f"Dream: {title_a[:30]} <-> {title_b[:30]}",
|
|
330
|
-
domain_str
|
|
346
|
+
domain_str,
|
|
347
|
+
*initial_memory_profile("dream_insight", store="ltm"))
|
|
331
348
|
)
|
|
332
349
|
insight_id = cur.lastrowid
|
|
333
350
|
|