nexo-brain 7.15.1 → 7.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/dashboard/app.py +71 -0
- package/src/dashboard/templates/memory.html +140 -0
- package/src/db/__init__.py +16 -0
- package/src/db/_memory_v2.py +1044 -0
- package/src/db/_schema.py +243 -0
- package/src/doctor/providers/runtime.py +16 -7
- package/src/hooks/post_edit_change_log.py +34 -2
- package/src/local_model_manifest.json +17 -0
- package/src/memory_observation_worker.py +31 -0
- package/src/memory_retrieval.py +278 -0
- package/src/model_warmup.py +9 -0
- package/src/plugins/protocol.py +43 -0
- package/src/server.py +115 -0
- package/src/tools_memory_v2.py +182 -0
- package/src/tools_sessions.py +16 -1
- package/tool-enforcement-map.json +110 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.16.0",
|
|
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
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `7.
|
|
21
|
+
Version `7.16.0` is the current packaged-runtime line. Minor release over v7.15.2 - Brain adds Memory Observations v2: evidence-backed event capture, derived observations, update-safe backfill, MCP retrieval, dashboard visibility, and safer refusal when memory lacks evidence.
|
|
22
|
+
|
|
23
|
+
Previously in `7.15.2`: patch release over v7.15.1 - Brain treats normal Codex startup context reads of calibration and project atlas files as healthy bootstrap activity instead of conditioned-file drift.
|
|
24
|
+
|
|
25
|
+
Previously in `7.15.1`: patch release over v7.15.0 - Brain drains larger self-audit clusters, bounds hook history with update-time cleanup, filters normal Codex bootstrap reads, routes email-monitor effort by message complexity, and locks morning briefings by local date and recipient.
|
|
22
26
|
|
|
23
27
|
Previously in `7.15.0`: minor release — Brain unifies sent-email continuity across send paths, moves cognitive recall to multilingual embeddings, forces tagged learnings into context, hardens email loop guards and headless runners, exposes learning creation dates, and adds AUTO-N burst postmortems.
|
|
24
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.16.0",
|
|
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/dashboard/app.py
CHANGED
|
@@ -704,6 +704,77 @@ async def api_memories(
|
|
|
704
704
|
return {"query": q, "store": store, "count": len(serialized), "results": serialized}
|
|
705
705
|
|
|
706
706
|
|
|
707
|
+
@app.get("/api/memory/observations")
|
|
708
|
+
async def api_memory_observations(
|
|
709
|
+
q: str = Query("", description="Search query"),
|
|
710
|
+
observation_type: str = Query("", description="Filter by observation type"),
|
|
711
|
+
project_key: str = Query("", description="Filter by project key"),
|
|
712
|
+
limit: int = Query(20, ge=1, le=100),
|
|
713
|
+
):
|
|
714
|
+
"""Operational view of Memory Observations v2."""
|
|
715
|
+
db = _db()
|
|
716
|
+
try:
|
|
717
|
+
queue_result = db.process_memory_observation_queue(limit=50)
|
|
718
|
+
except Exception as exc:
|
|
719
|
+
queue_result = {"ok": False, "error": str(exc)}
|
|
720
|
+
observations = db.list_memory_observations(
|
|
721
|
+
query=q,
|
|
722
|
+
observation_type=observation_type,
|
|
723
|
+
project_key=project_key,
|
|
724
|
+
limit=limit,
|
|
725
|
+
)
|
|
726
|
+
events = db.list_memory_events(
|
|
727
|
+
query=q,
|
|
728
|
+
project_key=project_key,
|
|
729
|
+
limit=min(limit, 50),
|
|
730
|
+
)
|
|
731
|
+
return {
|
|
732
|
+
"query": q,
|
|
733
|
+
"observation_type": observation_type,
|
|
734
|
+
"project_key": project_key,
|
|
735
|
+
"count": len(observations),
|
|
736
|
+
"event_count": len(events),
|
|
737
|
+
"queue": queue_result,
|
|
738
|
+
"stats": db.memory_observation_stats(days=7),
|
|
739
|
+
"event_stats": db.memory_event_stats(days=7),
|
|
740
|
+
"observations": observations,
|
|
741
|
+
"events": events,
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
@app.get("/api/memory/search-v2")
|
|
746
|
+
async def api_memory_search_v2(
|
|
747
|
+
query: str = Query("", description="Search query"),
|
|
748
|
+
project_hint: str = Query("", description="Project hint"),
|
|
749
|
+
time_range: str = Query("", description="today, yesterday, anteayer, last N h/d"),
|
|
750
|
+
depth: str = Query("brief", description="brief, timeline, evidence, or raw"),
|
|
751
|
+
limit: int = Query(10, ge=1, le=50),
|
|
752
|
+
):
|
|
753
|
+
"""Evidence-first Memory Observations v2 search."""
|
|
754
|
+
if not query:
|
|
755
|
+
return {"query": query, "count": 0, "candidates": [], "has_evidence": False}
|
|
756
|
+
from memory_retrieval import memory_search
|
|
757
|
+
|
|
758
|
+
return memory_search(
|
|
759
|
+
query,
|
|
760
|
+
project_hint=project_hint,
|
|
761
|
+
time_range=time_range,
|
|
762
|
+
depth=depth,
|
|
763
|
+
limit=limit,
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
@app.post("/api/memory/backfill")
|
|
768
|
+
async def api_memory_backfill(
|
|
769
|
+
sources: str = Query("", description="Comma-separated source list"),
|
|
770
|
+
limit: int = Query(100, ge=1, le=1000),
|
|
771
|
+
):
|
|
772
|
+
"""Run idempotent Memory Observations v2 backfill from durable Brain tables."""
|
|
773
|
+
requested = [item.strip() for item in (sources or "").split(",") if item.strip()]
|
|
774
|
+
result = _db().backfill_memory_observations(sources=requested or None, limit=limit)
|
|
775
|
+
return result
|
|
776
|
+
|
|
777
|
+
|
|
707
778
|
@app.get("/api/somatic")
|
|
708
779
|
async def api_somatic():
|
|
709
780
|
"""Somatic marker risk scores."""
|
|
@@ -167,6 +167,70 @@
|
|
|
167
167
|
</div>
|
|
168
168
|
</div>
|
|
169
169
|
|
|
170
|
+
<!-- Memory Observations -->
|
|
171
|
+
<div class="mb-6">
|
|
172
|
+
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between mb-3">
|
|
173
|
+
<div>
|
|
174
|
+
<h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold flex items-center gap-2">
|
|
175
|
+
<span class="w-2 h-2 rounded-full bg-sky-400 glow-dot" style="color:#38bdf8"></span>
|
|
176
|
+
Memory Observations
|
|
177
|
+
</h2>
|
|
178
|
+
<p class="text-xs text-slate-600 mt-1">Evidence-backed operational memory captured from tasks, edits, and durable context.</p>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
181
|
+
<input type="text" id="obs-query" placeholder="Search observations..."
|
|
182
|
+
class="bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-xs text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-sky-500 w-56"
|
|
183
|
+
onkeydown="if(event.key==='Enter')loadMemoryObservations()">
|
|
184
|
+
<input type="text" id="obs-project" placeholder="Project"
|
|
185
|
+
class="bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-xs text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-sky-500 w-32"
|
|
186
|
+
onkeydown="if(event.key==='Enter')loadMemoryObservations()">
|
|
187
|
+
<button onclick="loadMemoryObservations()" class="px-3 py-1.5 text-xs bg-sky-600 text-white rounded-lg hover:bg-sky-500 transition-colors font-medium">Refresh</button>
|
|
188
|
+
<button onclick="runMemoryBackfill()" class="px-3 py-1.5 text-xs bg-slate-800 text-slate-200 border border-slate-700 rounded-lg hover:bg-slate-700 transition-colors font-medium">Backfill</button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
|
193
|
+
<div class="bg-slate-900/50 border border-slate-800/60 rounded-xl p-3">
|
|
194
|
+
<div class="text-[10px] uppercase tracking-wider text-slate-500">Observations</div>
|
|
195
|
+
<div class="text-2xl font-display font-bold text-sky-300" id="obs-count">--</div>
|
|
196
|
+
</div>
|
|
197
|
+
<div class="bg-slate-900/50 border border-slate-800/60 rounded-xl p-3">
|
|
198
|
+
<div class="text-[10px] uppercase tracking-wider text-slate-500">Raw Events</div>
|
|
199
|
+
<div class="text-2xl font-display font-bold text-violet-300" id="obs-event-count">--</div>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="bg-slate-900/50 border border-slate-800/60 rounded-xl p-3">
|
|
202
|
+
<div class="text-[10px] uppercase tracking-wider text-slate-500">Queue Pending</div>
|
|
203
|
+
<div class="text-2xl font-display font-bold text-amber-300" id="obs-queue-count">--</div>
|
|
204
|
+
</div>
|
|
205
|
+
<div class="bg-slate-900/50 border border-slate-800/60 rounded-xl p-3">
|
|
206
|
+
<div class="text-[10px] uppercase tracking-wider text-slate-500">Last Process</div>
|
|
207
|
+
<div class="text-2xl font-display font-bold text-emerald-300" id="obs-processed-count">--</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div class="grid grid-cols-1 xl:grid-cols-2 gap-5">
|
|
212
|
+
<div class="bg-sky-500/5 border border-sky-500/20 rounded-xl p-4">
|
|
213
|
+
<div class="flex items-center justify-between mb-3">
|
|
214
|
+
<div class="text-xs uppercase tracking-wider text-sky-400 font-semibold">Observations</div>
|
|
215
|
+
<div class="text-[10px] text-slate-500 font-mono" id="obs-list-count">--</div>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="space-y-2" id="obs-list">
|
|
218
|
+
<div class="text-xs text-slate-600 text-center py-6">Loading...</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div class="bg-violet-500/5 border border-violet-500/20 rounded-xl p-4">
|
|
223
|
+
<div class="flex items-center justify-between mb-3">
|
|
224
|
+
<div class="text-xs uppercase tracking-wider text-violet-400 font-semibold">Evidence Events</div>
|
|
225
|
+
<div class="text-[10px] text-slate-500 font-mono" id="obs-raw-count">--</div>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="space-y-2" id="obs-events">
|
|
228
|
+
<div class="text-xs text-slate-600 text-center py-6">Loading...</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
170
234
|
<!-- Two-column: STM and LTM Recent -->
|
|
171
235
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5 mb-6" id="columns-area">
|
|
172
236
|
<!-- Recent STM -->
|
|
@@ -313,6 +377,41 @@
|
|
|
313
377
|
</div>`;
|
|
314
378
|
}
|
|
315
379
|
|
|
380
|
+
function renderObservationCard(item) {
|
|
381
|
+
const refs = item.evidence_refs || [];
|
|
382
|
+
const entities = item.entities || [];
|
|
383
|
+
const created = item.created_at ? recentTime(item.created_at) : '';
|
|
384
|
+
const score = item.salience != null ? Number(item.salience).toFixed(2) : '--';
|
|
385
|
+
return `<div class="bg-slate-900/50 border border-sky-500/15 rounded-xl p-4">
|
|
386
|
+
<div class="flex items-center gap-2 mb-2 flex-wrap">
|
|
387
|
+
<span class="text-[10px] px-1.5 py-0.5 rounded bg-sky-500/10 text-sky-400 font-medium">${escapeHtml(item.observation_type || 'observation')}</span>
|
|
388
|
+
${item.project_key ? `<span class="text-[10px] px-1.5 py-0.5 rounded bg-slate-800 text-slate-500">${escapeHtml(item.project_key)}</span>` : ''}
|
|
389
|
+
<span class="text-[10px] text-slate-600 ml-auto font-mono">${escapeHtml(created)}</span>
|
|
390
|
+
</div>
|
|
391
|
+
<div class="text-sm text-slate-200 leading-relaxed">${escapeHtml(item.summary || '')}</div>
|
|
392
|
+
<div class="flex items-center gap-2 mt-2 flex-wrap">
|
|
393
|
+
<span class="text-[10px] text-slate-600 font-mono">score ${escapeHtml(score)}</span>
|
|
394
|
+
${entities.slice(0, 3).map(e => `<span class="text-[10px] px-1.5 py-0.5 rounded bg-slate-800 text-slate-500">${escapeHtml(e)}</span>`).join('')}
|
|
395
|
+
</div>
|
|
396
|
+
<div class="text-[10px] text-slate-600 font-mono mt-2 break-all">${escapeHtml(refs.slice(0, 3).join(' | '))}</div>
|
|
397
|
+
</div>`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function renderMemoryEventCard(item) {
|
|
401
|
+
const created = item.created_at ? recentTime(item.created_at) : '';
|
|
402
|
+
const paths = item.file_paths || [];
|
|
403
|
+
const source = `${item.source_type || '?'}:${item.source_id || ''}`;
|
|
404
|
+
return `<div class="bg-slate-900/50 border border-violet-500/15 rounded-xl p-4">
|
|
405
|
+
<div class="flex items-center gap-2 mb-2 flex-wrap">
|
|
406
|
+
<span class="text-[10px] px-1.5 py-0.5 rounded bg-violet-500/10 text-violet-400 font-medium">${escapeHtml(item.event_type || 'event')}</span>
|
|
407
|
+
<span class="text-[10px] text-slate-600 ml-auto font-mono">${escapeHtml(created)}</span>
|
|
408
|
+
</div>
|
|
409
|
+
<div class="text-sm text-slate-200 leading-relaxed">${escapeHtml(source)}</div>
|
|
410
|
+
${paths.length ? `<div class="text-xs text-slate-500 mt-2">${escapeHtml(paths.slice(0, 4).join(', '))}</div>` : ''}
|
|
411
|
+
<div class="text-[10px] text-slate-600 font-mono mt-2 break-all">${escapeHtml(item.event_uid || '')}</div>
|
|
412
|
+
</div>`;
|
|
413
|
+
}
|
|
414
|
+
|
|
316
415
|
async function loadRecentContext() {
|
|
317
416
|
const q = document.getElementById('context-query').value.trim();
|
|
318
417
|
const url = '/api/recent-context?hours=24&limit=8' + (q ? `&query=${encodeURIComponent(q)}` : '');
|
|
@@ -332,6 +431,45 @@
|
|
|
332
431
|
: events.map(renderHotEventCard).join('');
|
|
333
432
|
}
|
|
334
433
|
|
|
434
|
+
async function loadMemoryObservations() {
|
|
435
|
+
const q = document.getElementById('obs-query').value.trim();
|
|
436
|
+
const project = document.getElementById('obs-project').value.trim();
|
|
437
|
+
const url = '/api/memory/observations?limit=12'
|
|
438
|
+
+ (q ? `&q=${encodeURIComponent(q)}` : '')
|
|
439
|
+
+ (project ? `&project_key=${encodeURIComponent(project)}` : '');
|
|
440
|
+
const data = await fetchJSON(url);
|
|
441
|
+
if (!data) return;
|
|
442
|
+
|
|
443
|
+
const stats = data.stats || {};
|
|
444
|
+
const queue = stats.queue || {};
|
|
445
|
+
const processed = (data.queue && data.queue.processed) || 0;
|
|
446
|
+
const observations = data.observations || [];
|
|
447
|
+
const events = data.events || [];
|
|
448
|
+
document.getElementById('obs-count').textContent = formatNumber(stats.total || observations.length);
|
|
449
|
+
document.getElementById('obs-event-count').textContent = formatNumber((data.event_stats && data.event_stats.total) || events.length);
|
|
450
|
+
document.getElementById('obs-queue-count').textContent = formatNumber(queue.pending || 0);
|
|
451
|
+
document.getElementById('obs-processed-count').textContent = formatNumber(processed);
|
|
452
|
+
document.getElementById('obs-list-count').textContent = formatNumber(observations.length);
|
|
453
|
+
document.getElementById('obs-raw-count').textContent = formatNumber(events.length);
|
|
454
|
+
document.getElementById('obs-list').innerHTML = observations.length === 0
|
|
455
|
+
? '<div class="text-xs text-slate-600 text-center py-6">No observations found</div>'
|
|
456
|
+
: observations.map(renderObservationCard).join('');
|
|
457
|
+
document.getElementById('obs-events').innerHTML = events.length === 0
|
|
458
|
+
? '<div class="text-xs text-slate-600 text-center py-6">No evidence events found</div>'
|
|
459
|
+
: events.map(renderMemoryEventCard).join('');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function runMemoryBackfill() {
|
|
463
|
+
const res = await fetch('/api/memory/backfill?sources=protocol_tasks,change_log,session_diary,recent_events&limit=200', { method: 'POST' });
|
|
464
|
+
if (!res.ok) {
|
|
465
|
+
showToast('Backfill failed');
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const data = await res.json();
|
|
469
|
+
showToast(`Backfill ${data.created_or_updated || 0}/${data.seen || 0}`);
|
|
470
|
+
loadMemoryObservations();
|
|
471
|
+
}
|
|
472
|
+
|
|
335
473
|
async function loadFlow() {
|
|
336
474
|
const data = await fetchJSON('/api/memory/flow');
|
|
337
475
|
if (!data) return;
|
|
@@ -413,8 +551,10 @@
|
|
|
413
551
|
|
|
414
552
|
// Init
|
|
415
553
|
loadRecentContext();
|
|
554
|
+
loadMemoryObservations();
|
|
416
555
|
loadFlow();
|
|
417
556
|
setInterval(loadRecentContext, REFRESH_MS);
|
|
557
|
+
setInterval(loadMemoryObservations, REFRESH_MS);
|
|
418
558
|
setInterval(loadFlow, REFRESH_MS);
|
|
419
559
|
</script>
|
|
420
560
|
{% endblock %}
|
package/src/db/__init__.py
CHANGED
|
@@ -55,6 +55,7 @@ _drive = _load_submodule("db._drive")
|
|
|
55
55
|
_outcomes = _load_submodule("db._outcomes")
|
|
56
56
|
_goal_profiles = _load_submodule("db._goal_profiles")
|
|
57
57
|
_continuity = _load_submodule("db._continuity")
|
|
58
|
+
_memory_v2 = _load_submodule("db._memory_v2")
|
|
58
59
|
|
|
59
60
|
# Core: connection, constants, init, utils
|
|
60
61
|
from db._core import (
|
|
@@ -93,6 +94,21 @@ from db._continuity import (
|
|
|
93
94
|
latest_continuity_snapshot,
|
|
94
95
|
)
|
|
95
96
|
|
|
97
|
+
from db._memory_v2 import (
|
|
98
|
+
build_memory_event_uid,
|
|
99
|
+
record_memory_event,
|
|
100
|
+
list_memory_events,
|
|
101
|
+
memory_event_stats,
|
|
102
|
+
upsert_memory_observation,
|
|
103
|
+
process_memory_observation_queue,
|
|
104
|
+
list_memory_observations,
|
|
105
|
+
search_memory_observations_fts,
|
|
106
|
+
backfill_memory_observations,
|
|
107
|
+
memory_observation_health,
|
|
108
|
+
maintain_memory_observations,
|
|
109
|
+
memory_observation_stats,
|
|
110
|
+
)
|
|
111
|
+
|
|
96
112
|
# PostToolUse inbox-reminder rate limit (v6.0.1)
|
|
97
113
|
_hook_inbox_reminders = _load_submodule("db._hook_inbox_reminders")
|
|
98
114
|
from db._hook_inbox_reminders import (
|