nexo-brain 1.0.0 → 1.1.1
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/README.md +79 -13
- package/package.json +1 -1
- package/src/db.py +93 -0
- package/src/hooks/post-compact.sh +147 -0
- package/src/hooks/pre-compact.sh +40 -56
- package/src/server.py +71 -0
- package/src/tools_sessions.py +11 -0
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# NEXO Brain — Your AI Gets a Brain
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/nexo-brain)
|
|
4
4
|
[](https://github.com/wazionapps/nexo/blob/main/benchmarks/locomo/results/)
|
|
5
5
|
[](https://github.com/snap-research/locomo/issues/33)
|
|
6
6
|
[](https://github.com/wazionapps/nexo/stargazers)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
|
|
9
|
-
> **v1.
|
|
9
|
+
> **v1.1.0** — Context Continuity via auto-compaction hooks. PreCompact saves a full session checkpoint; PostCompact re-injects it so long sessions (8+ hours) feel like one continuous conversation. Plus: Cognitive Cortex, 30 Core Rules as DNA, Smart Startup, Context Packets, Auto-Prime. The first AI memory system with architectural inhibitory control — the agent reasons about whether to act before acting. Battle-tested from 6 months of production use, validated via multi-AI debate (Claude Opus + GPT-5.4 + Gemini 3.1 Pro).
|
|
10
10
|
|
|
11
11
|
**NEXO Brain transforms any MCP-compatible AI agent from a stateless assistant into a cognitive partner that remembers, learns, forgets, adapts, and builds a relationship with you over time.**
|
|
12
12
|
|
|
@@ -163,9 +163,42 @@ User message → Fast Path check → Simple chat? → Respond directly
|
|
|
163
163
|
|
|
164
164
|
The Cortex was designed through a 3-way AI debate (Claude Opus 4.6 + GPT-5.4 + Gemini 3.1 Pro) and validated against 6 months of real production failures.
|
|
165
165
|
|
|
166
|
+
## Context Continuity (Auto-Compaction) (v1.1.0)
|
|
167
|
+
|
|
168
|
+
NEXO Brain automatically preserves session context when Claude Code compacts conversations. Using PreCompact and PostCompact hooks:
|
|
169
|
+
|
|
170
|
+
- **PreCompact**: Saves a complete session checkpoint to SQLite (task, files, decisions, errors, reasoning thread, next step)
|
|
171
|
+
- **PostCompact**: Re-injects a structured Core Memory Block into the conversation, so the session continues seamlessly
|
|
172
|
+
|
|
173
|
+
This means long sessions (8+ hours) feel like one continuous conversation instead of restarting after each compaction.
|
|
174
|
+
|
|
175
|
+
**How it works:**
|
|
176
|
+
1. Configure the hooks in your Claude Code `settings.json`
|
|
177
|
+
2. NEXO Brain's heartbeat automatically maintains the checkpoint
|
|
178
|
+
3. When compaction happens, the PreCompact hook reads the checkpoint and injects a recovery block
|
|
179
|
+
4. The session continues from exactly where it left off
|
|
180
|
+
|
|
181
|
+
**Setup:**
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"hooks": {
|
|
185
|
+
"PreCompact": [{
|
|
186
|
+
"matcher": "*",
|
|
187
|
+
"hooks": [{"type": "command", "command": "bash path/to/nexo/src/hooks/pre-compact.sh", "timeout": 10}]
|
|
188
|
+
}],
|
|
189
|
+
"PostCompact": [{
|
|
190
|
+
"matcher": "*",
|
|
191
|
+
"hooks": [{"type": "command", "command": "bash path/to/nexo/src/hooks/post-compact.sh", "timeout": 10}]
|
|
192
|
+
}]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
2 new MCP tools: `nexo_checkpoint_save` (manual or hook-triggered checkpoint), `nexo_checkpoint_read` (retrieves the latest checkpoint for context injection).
|
|
198
|
+
|
|
166
199
|
## Cognitive Features
|
|
167
200
|
|
|
168
|
-
NEXO Brain provides 29 cognitive tools on top of the
|
|
201
|
+
NEXO Brain provides 29 cognitive tools on top of the 78 base tools, totaling **115+ MCP tools**. These features implement cognitive science concepts that go beyond basic memory:
|
|
169
202
|
|
|
170
203
|
### Input Pipeline
|
|
171
204
|
|
|
@@ -238,7 +271,7 @@ Full results in [`benchmarks/locomo/results/`](benchmarks/locomo/results/).
|
|
|
238
271
|
|
|
239
272
|
Memory alone doesn't make a co-operator. What makes the difference is the **behavioral loop** — the automated discipline that ensures every session starts informed, runs with guardrails, and ends with self-reflection.
|
|
240
273
|
|
|
241
|
-
###
|
|
274
|
+
### 6 Automated Hooks
|
|
242
275
|
|
|
243
276
|
These fire automatically at key moments in every Claude Code session:
|
|
244
277
|
|
|
@@ -247,7 +280,8 @@ These fire automatically at key moments in every Claude Code session:
|
|
|
247
280
|
| **SessionStart** | Session opens | Generates a briefing from SQLite: overdue reminders, today's tasks, pending followups, active sessions |
|
|
248
281
|
| **Stop** | Session ends | Mandatory post-mortem: self-critique (5 questions), session buffer entry, followup creation, proactive seeds for next session |
|
|
249
282
|
| **PostToolUse** | After each tool call | Captures meaningful mutations to the Sensory Register |
|
|
250
|
-
| **PreCompact** | Before context compression | Saves
|
|
283
|
+
| **PreCompact** | Before context compression | Saves full session checkpoint to SQLite — task, files, decisions, errors, reasoning thread |
|
|
284
|
+
| **PostCompact** | After context compression | Re-injects Core Memory Block so the session continues seamlessly from where it left off |
|
|
251
285
|
| **Caffeinate** | Always (optional) | Keeps Mac awake for nocturnal cognitive processes |
|
|
252
286
|
|
|
253
287
|
### The Session Lifecycle
|
|
@@ -263,7 +297,9 @@ Heartbeat on every interaction (sentiment, context shifts)
|
|
|
263
297
|
↓
|
|
264
298
|
Guard check before every code edit
|
|
265
299
|
↓
|
|
266
|
-
PreCompact hook saves
|
|
300
|
+
PreCompact hook saves full checkpoint if conversation is compressed
|
|
301
|
+
↓
|
|
302
|
+
PostCompact hook re-injects Core Memory Block → session continues seamlessly
|
|
267
303
|
↓
|
|
268
304
|
Stop hook triggers mandatory post-mortem:
|
|
269
305
|
- Self-critique: 5 questions about what could be better
|
|
@@ -346,7 +382,7 @@ The installer handles everything:
|
|
|
346
382
|
- Node.js project detected
|
|
347
383
|
Configuring MCP server...
|
|
348
384
|
Setting up automated processes...
|
|
349
|
-
|
|
385
|
+
6 automated processes configured.
|
|
350
386
|
Caffeinate enabled.
|
|
351
387
|
Generating operator instructions...
|
|
352
388
|
|
|
@@ -370,25 +406,25 @@ That's it. No need to run `claude` manually. Atlas will greet you immediately
|
|
|
370
406
|
| Component | What | Where |
|
|
371
407
|
|-----------|------|-------|
|
|
372
408
|
| Cognitive engine | Python: fastembed, numpy, vector search | pip packages |
|
|
373
|
-
| MCP server |
|
|
409
|
+
| MCP server | 111+ tools for memory, cognition, learning, guard | ~/.nexo/ |
|
|
374
410
|
| Plugins | Guard, episodic memory, cognitive memory, entities, preferences | ~/.nexo/plugins/ |
|
|
375
|
-
| Hooks (
|
|
411
|
+
| Hooks (6) | SessionStart briefing, Stop post-mortem, PostToolUse capture, PreCompact checkpoint, PostCompact recovery, Caffeinate | ~/.nexo/hooks/ |
|
|
376
412
|
| Reflection engine | Processes session buffer, extracts patterns, updates user model | ~/.nexo/scripts/ |
|
|
377
413
|
| CLAUDE.md | Complete operator instructions (Codex, hooks, guard, trust, memory) | ~/.claude/CLAUDE.md |
|
|
378
414
|
| LaunchAgents | Decay, sleep, audit, postmortem, catch-up | ~/Library/LaunchAgents/ |
|
|
379
415
|
| Auto-update | Checks for new versions at boot | Built into catch-up |
|
|
380
|
-
| Claude Code config | MCP server +
|
|
416
|
+
| Claude Code config | MCP server + 6 hooks registered | ~/.claude/settings.json |
|
|
381
417
|
|
|
382
418
|
### Requirements
|
|
383
419
|
|
|
384
420
|
- **macOS or Linux** (Windows via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install))
|
|
385
421
|
- **Node.js 18+** (for the installer)
|
|
386
|
-
- **Claude Opus (latest version) strongly recommended.** NEXO Brain provides
|
|
422
|
+
- **Claude Opus (latest version) strongly recommended.** NEXO Brain provides 111+ MCP tools across 20 categories. This cognitive load requires a top-tier model with large context window. Smaller models (Haiku, Sonnet) may struggle with tool selection and produce inconsistent results. Opus handles all 111+ tools without hesitation.
|
|
387
423
|
- Python 3, Homebrew, and Claude Code are installed automatically if missing.
|
|
388
424
|
|
|
389
425
|
## Architecture
|
|
390
426
|
|
|
391
|
-
###
|
|
427
|
+
### 111+ MCP Tools across 20 Categories
|
|
392
428
|
|
|
393
429
|
| Category | Count | Tools | Purpose |
|
|
394
430
|
|----------|-------|-------|---------|
|
|
@@ -412,6 +448,7 @@ That's it. No need to run `claude` manually. Atlas will greet you immediately
|
|
|
412
448
|
| Evolution | 5 | propose, approve, reject, status, history | Self-improvement proposals |
|
|
413
449
|
| Adaptive & Somatic | 4 | adaptive_weights, adaptive_override, somatic_check, somatic_stats | Learned signal weights + pain memory per file |
|
|
414
450
|
| Knowledge Graph | 4 | kg_query, kg_path, kg_neighbors, kg_stats | Bi-temporal entity-relationship graph |
|
|
451
|
+
| Context Continuity | 2 | checkpoint_save, checkpoint_read | Auto-compaction session preservation |
|
|
415
452
|
|
|
416
453
|
### Plugin System
|
|
417
454
|
|
|
@@ -469,7 +506,7 @@ NEXO Brain is designed as an MCP server. Claude Code is the primary supported cl
|
|
|
469
506
|
npx nexo-brain
|
|
470
507
|
```
|
|
471
508
|
|
|
472
|
-
All
|
|
509
|
+
All 111+ tools are available immediately after installation. The installer configures Claude Code's `~/.claude/settings.json` automatically.
|
|
473
510
|
|
|
474
511
|
### OpenClaw
|
|
475
512
|
|
|
@@ -565,6 +602,35 @@ If NEXO Brain is useful to you, consider:
|
|
|
565
602
|
|
|
566
603
|
[](https://star-history.com/#wazionapps/nexo&Date)
|
|
567
604
|
|
|
605
|
+
## Changelog
|
|
606
|
+
|
|
607
|
+
### v1.1.0 — Context Continuity (2026-03-27)
|
|
608
|
+
- **Context Continuity**: PreCompact/PostCompact hooks preserve session state across compaction events
|
|
609
|
+
- New `session_checkpoints` SQLite table + migration #12
|
|
610
|
+
- New tools: `nexo_checkpoint_save`, `nexo_checkpoint_read`
|
|
611
|
+
- Heartbeat automatically maintains checkpoint every interaction
|
|
612
|
+
- Core Memory Block re-injected post-compaction with task, files, decisions, reasoning thread
|
|
613
|
+
- 115+ total tools, 20 categories, 6 hooks
|
|
614
|
+
|
|
615
|
+
### v1.0.0 — Cognitive Cortex + Stable Release (2026-03-26)
|
|
616
|
+
- **Cognitive Cortex**: architectural inhibitory control (ASK/PROPOSE/ACT modes)
|
|
617
|
+
- 30 Core Rules as immutable DNA in SQLite
|
|
618
|
+
- Designed via 3-way AI debate (Claude Opus + GPT-5.4 + Gemini 3.1 Pro)
|
|
619
|
+
- Artifact Registry for operational facts
|
|
620
|
+
- Full benchmark suite (LoCoMo F1: 0.588)
|
|
621
|
+
|
|
622
|
+
### v0.10.0 — Smart Context (2026-03-22)
|
|
623
|
+
- Smart Startup: pre-loads memories from pending followups + diary
|
|
624
|
+
- Context Packet: structured injection for subagents
|
|
625
|
+
- Auto-Prime: keyword-triggered area learnings in heartbeat
|
|
626
|
+
- Diary Archive: permanent subconscious memory (180d+ auto-archived)
|
|
627
|
+
|
|
628
|
+
### v0.9.0 — Cognitive Memory (2026-03-15)
|
|
629
|
+
- Atkinson-Shiffrin memory model (STM → LTM promotion)
|
|
630
|
+
- Semantic RAG with fastembed (BAAI/bge-small-en-v1.5, 384 dims)
|
|
631
|
+
- Trust scoring, sentiment detection, adaptive personality modes
|
|
632
|
+
- Ebbinghaus decay, sister detection, quarantine system
|
|
633
|
+
|
|
568
634
|
## License
|
|
569
635
|
|
|
570
636
|
MIT -- see [LICENSE](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO \u2014 Cognitive co-operator for Claude Code. Atkinson-Shiffrin memory, semantic RAG, trust scoring, and metacognitive error prevention.",
|
|
6
6
|
"bin": {
|
package/src/db.py
CHANGED
|
@@ -1003,6 +1003,30 @@ def _seed_core_rules(conn):
|
|
|
1003
1003
|
conn.commit()
|
|
1004
1004
|
|
|
1005
1005
|
|
|
1006
|
+
def _m12_session_checkpoints(conn):
|
|
1007
|
+
"""Session checkpoints for intelligent auto-compaction.
|
|
1008
|
+
|
|
1009
|
+
PreCompact saves a checkpoint; PostCompact reads it to re-inject a
|
|
1010
|
+
Core Memory Block that preserves continuity after context compression.
|
|
1011
|
+
"""
|
|
1012
|
+
conn.execute("""
|
|
1013
|
+
CREATE TABLE IF NOT EXISTS session_checkpoints (
|
|
1014
|
+
sid TEXT PRIMARY KEY,
|
|
1015
|
+
task TEXT DEFAULT '',
|
|
1016
|
+
task_status TEXT DEFAULT 'active',
|
|
1017
|
+
active_files TEXT DEFAULT '[]',
|
|
1018
|
+
current_goal TEXT DEFAULT '',
|
|
1019
|
+
decisions_summary TEXT DEFAULT '',
|
|
1020
|
+
errors_found TEXT DEFAULT '',
|
|
1021
|
+
reasoning_thread TEXT DEFAULT '',
|
|
1022
|
+
next_step TEXT DEFAULT '',
|
|
1023
|
+
compaction_count INTEGER DEFAULT 0,
|
|
1024
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1025
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
1026
|
+
)
|
|
1027
|
+
""")
|
|
1028
|
+
|
|
1029
|
+
|
|
1006
1030
|
# Migration registry — APPEND ONLY, never reorder or delete
|
|
1007
1031
|
MIGRATIONS = [
|
|
1008
1032
|
(1, "learnings_columns", _m1_learnings_columns),
|
|
@@ -1016,6 +1040,7 @@ MIGRATIONS = [
|
|
|
1016
1040
|
(9, "maintenance_schedule", _m9_maintenance_schedule),
|
|
1017
1041
|
(10, "diary_archive", _m10_diary_archive),
|
|
1018
1042
|
(11, "core_rules", _m11_core_rules),
|
|
1043
|
+
(12, "session_checkpoints", _m12_session_checkpoints),
|
|
1019
1044
|
]
|
|
1020
1045
|
|
|
1021
1046
|
|
|
@@ -2814,3 +2839,71 @@ def update_evolution_log_status(log_id: int, status: str, **kwargs):
|
|
|
2814
2839
|
vals.append(kwargs[k])
|
|
2815
2840
|
vals.append(log_id)
|
|
2816
2841
|
conn.execute(f"UPDATE evolution_log SET {', '.join(sets)} WHERE id = ?", vals)
|
|
2842
|
+
|
|
2843
|
+
|
|
2844
|
+
# ── Session Checkpoint operations ──────────────────────────────────
|
|
2845
|
+
|
|
2846
|
+
def save_checkpoint(sid: str, task: str = '', task_status: str = 'active',
|
|
2847
|
+
active_files: str = '[]', current_goal: str = '',
|
|
2848
|
+
decisions_summary: str = '', errors_found: str = '',
|
|
2849
|
+
reasoning_thread: str = '', next_step: str = '') -> dict:
|
|
2850
|
+
"""Save or update a session checkpoint. Called by PreCompact hook."""
|
|
2851
|
+
conn = get_db()
|
|
2852
|
+
# Get current compaction count
|
|
2853
|
+
existing = conn.execute(
|
|
2854
|
+
"SELECT compaction_count FROM session_checkpoints WHERE sid = ?", (sid,)
|
|
2855
|
+
).fetchone()
|
|
2856
|
+
count = (existing["compaction_count"] + 1) if existing else 0
|
|
2857
|
+
|
|
2858
|
+
conn.execute(
|
|
2859
|
+
"""INSERT INTO session_checkpoints
|
|
2860
|
+
(sid, task, task_status, active_files, current_goal,
|
|
2861
|
+
decisions_summary, errors_found, reasoning_thread, next_step,
|
|
2862
|
+
compaction_count, updated_at)
|
|
2863
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
2864
|
+
ON CONFLICT(sid) DO UPDATE SET
|
|
2865
|
+
task = excluded.task,
|
|
2866
|
+
task_status = excluded.task_status,
|
|
2867
|
+
active_files = excluded.active_files,
|
|
2868
|
+
current_goal = excluded.current_goal,
|
|
2869
|
+
decisions_summary = excluded.decisions_summary,
|
|
2870
|
+
errors_found = excluded.errors_found,
|
|
2871
|
+
reasoning_thread = excluded.reasoning_thread,
|
|
2872
|
+
next_step = excluded.next_step,
|
|
2873
|
+
compaction_count = excluded.compaction_count,
|
|
2874
|
+
updated_at = datetime('now')""",
|
|
2875
|
+
(sid, task, task_status, active_files, current_goal,
|
|
2876
|
+
decisions_summary, errors_found, reasoning_thread, next_step, count)
|
|
2877
|
+
)
|
|
2878
|
+
conn.commit()
|
|
2879
|
+
return {"sid": sid, "compaction_count": count}
|
|
2880
|
+
|
|
2881
|
+
|
|
2882
|
+
def read_checkpoint(sid: str = '') -> dict | None:
|
|
2883
|
+
"""Read the most recent session checkpoint. If no sid, returns the latest."""
|
|
2884
|
+
conn = get_db()
|
|
2885
|
+
if sid:
|
|
2886
|
+
row = conn.execute(
|
|
2887
|
+
"SELECT * FROM session_checkpoints WHERE sid = ?", (sid,)
|
|
2888
|
+
).fetchone()
|
|
2889
|
+
else:
|
|
2890
|
+
row = conn.execute(
|
|
2891
|
+
"SELECT * FROM session_checkpoints ORDER BY updated_at DESC LIMIT 1"
|
|
2892
|
+
).fetchone()
|
|
2893
|
+
return dict(row) if row else None
|
|
2894
|
+
|
|
2895
|
+
|
|
2896
|
+
def increment_compaction_count(sid: str) -> int:
|
|
2897
|
+
"""Increment and return the compaction count for a session."""
|
|
2898
|
+
conn = get_db()
|
|
2899
|
+
conn.execute(
|
|
2900
|
+
"""UPDATE session_checkpoints
|
|
2901
|
+
SET compaction_count = compaction_count + 1, updated_at = datetime('now')
|
|
2902
|
+
WHERE sid = ?""",
|
|
2903
|
+
(sid,)
|
|
2904
|
+
)
|
|
2905
|
+
conn.commit()
|
|
2906
|
+
row = conn.execute(
|
|
2907
|
+
"SELECT compaction_count FROM session_checkpoints WHERE sid = ?", (sid,)
|
|
2908
|
+
).fetchone()
|
|
2909
|
+
return row["compaction_count"] if row else 0
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# NEXO PostCompact Hook — Re-inject Core Memory Block after compaction
|
|
3
|
+
# Reads the latest session checkpoint from SQLite and generates a structured
|
|
4
|
+
# context block that preserves session continuity.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
8
|
+
NEXO_DB="$NEXO_HOME/nexo.db"
|
|
9
|
+
TODAY=$(date +%Y-%m-%d)
|
|
10
|
+
LOG_FILE="$NEXO_HOME/operations/tool-logs/${TODAY}.jsonl"
|
|
11
|
+
LOG_LINES=0
|
|
12
|
+
if [ -f "$LOG_FILE" ]; then
|
|
13
|
+
LOG_LINES=$(wc -l < "$LOG_FILE" | tr -d ' ')
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Read checkpoint for the session that just compacted
|
|
17
|
+
# PreCompact writes the SID to /tmp/nexo-compacting-sid
|
|
18
|
+
TARGET_SID=""
|
|
19
|
+
if [ -f /tmp/nexo-compacting-sid ]; then
|
|
20
|
+
TARGET_SID=$(cat /tmp/nexo-compacting-sid 2>/dev/null || echo "")
|
|
21
|
+
rm -f /tmp/nexo-compacting-sid
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
CHECKPOINT=""
|
|
25
|
+
if [ -f "$NEXO_DB" ]; then
|
|
26
|
+
if [ -n "$TARGET_SID" ]; then
|
|
27
|
+
# Read checkpoint for the specific session that compacted
|
|
28
|
+
CHECKPOINT=$(sqlite3 "$NEXO_DB" "
|
|
29
|
+
SELECT sid, task, task_status, active_files, current_goal,
|
|
30
|
+
decisions_summary, errors_found, reasoning_thread,
|
|
31
|
+
next_step, compaction_count
|
|
32
|
+
FROM session_checkpoints
|
|
33
|
+
WHERE sid = '$TARGET_SID'
|
|
34
|
+
" 2>/dev/null || echo "")
|
|
35
|
+
fi
|
|
36
|
+
# Fallback: if no target SID or no checkpoint found, use latest
|
|
37
|
+
if [ -z "$CHECKPOINT" ]; then
|
|
38
|
+
CHECKPOINT=$(sqlite3 "$NEXO_DB" "
|
|
39
|
+
SELECT sid, task, task_status, active_files, current_goal,
|
|
40
|
+
decisions_summary, errors_found, reasoning_thread,
|
|
41
|
+
next_step, compaction_count
|
|
42
|
+
FROM session_checkpoints
|
|
43
|
+
ORDER BY updated_at DESC LIMIT 1
|
|
44
|
+
" 2>/dev/null || echo "")
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
if [ -n "$CHECKPOINT" ]; then
|
|
48
|
+
# Parse pipe-separated fields
|
|
49
|
+
SID=$(echo "$CHECKPOINT" | cut -d'|' -f1)
|
|
50
|
+
TASK=$(echo "$CHECKPOINT" | cut -d'|' -f2)
|
|
51
|
+
TASK_STATUS=$(echo "$CHECKPOINT" | cut -d'|' -f3)
|
|
52
|
+
ACTIVE_FILES=$(echo "$CHECKPOINT" | cut -d'|' -f4)
|
|
53
|
+
CURRENT_GOAL=$(echo "$CHECKPOINT" | cut -d'|' -f5)
|
|
54
|
+
DECISIONS=$(echo "$CHECKPOINT" | cut -d'|' -f6)
|
|
55
|
+
ERRORS=$(echo "$CHECKPOINT" | cut -d'|' -f7)
|
|
56
|
+
REASONING=$(echo "$CHECKPOINT" | cut -d'|' -f8)
|
|
57
|
+
NEXT_STEP=$(echo "$CHECKPOINT" | cut -d'|' -f9)
|
|
58
|
+
COMPACT_COUNT=$(echo "$CHECKPOINT" | cut -d'|' -f10)
|
|
59
|
+
|
|
60
|
+
# Increment compaction count
|
|
61
|
+
sqlite3 "$NEXO_DB" "
|
|
62
|
+
UPDATE session_checkpoints
|
|
63
|
+
SET compaction_count = compaction_count + 1, updated_at = datetime('now')
|
|
64
|
+
WHERE sid = '$SID'
|
|
65
|
+
" 2>/dev/null || true
|
|
66
|
+
|
|
67
|
+
# Read diary draft for extra context
|
|
68
|
+
DRAFT=$(sqlite3 "$NEXO_DB" "
|
|
69
|
+
SELECT tasks_seen, last_context_hint
|
|
70
|
+
FROM session_diary_draft
|
|
71
|
+
WHERE sid = '$SID'
|
|
72
|
+
" 2>/dev/null || echo "")
|
|
73
|
+
|
|
74
|
+
TASKS_SEEN=""
|
|
75
|
+
LAST_HINT=""
|
|
76
|
+
if [ -n "$DRAFT" ]; then
|
|
77
|
+
TASKS_SEEN=$(echo "$DRAFT" | cut -d'|' -f1)
|
|
78
|
+
LAST_HINT=$(echo "$DRAFT" | cut -d'|' -f2)
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Build Core Memory Block
|
|
82
|
+
BLOCK="## SESSION CONTINUITY [auto-injected post-compaction #$((COMPACT_COUNT + 1))]"
|
|
83
|
+
BLOCK="$BLOCK\n**Session:** $SID"
|
|
84
|
+
BLOCK="$BLOCK\n**Task:** $TASK (status: $TASK_STATUS)"
|
|
85
|
+
|
|
86
|
+
if [ -n "$CURRENT_GOAL" ] && [ "$CURRENT_GOAL" != "$TASK" ]; then
|
|
87
|
+
BLOCK="$BLOCK\n**Goal:** $CURRENT_GOAL"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
if [ -n "$ACTIVE_FILES" ] && [ "$ACTIVE_FILES" != "[]" ]; then
|
|
91
|
+
BLOCK="$BLOCK\n**Files:** $ACTIVE_FILES"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if [ -n "$DECISIONS" ]; then
|
|
95
|
+
BLOCK="$BLOCK\n**Decisions:** $DECISIONS"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
if [ -n "$ERRORS" ]; then
|
|
99
|
+
BLOCK="$BLOCK\n**Errors:** $ERRORS"
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
if [ -n "$NEXT_STEP" ]; then
|
|
103
|
+
BLOCK="$BLOCK\n**Next:** $NEXT_STEP"
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
if [ -n "$REASONING" ]; then
|
|
107
|
+
BLOCK="$BLOCK\n**Context:** $REASONING"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
if [ -n "$LAST_HINT" ]; then
|
|
111
|
+
BLOCK="$BLOCK\n**Last context:** $LAST_HINT"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if [ -n "$TASKS_SEEN" ] && [ "$TASKS_SEEN" != "[]" ]; then
|
|
115
|
+
BLOCK="$BLOCK\n**Session tasks so far:** $TASKS_SEEN"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
BLOCK="$BLOCK\n**Tool logs:** ${NEXO_HOME}/operations/tool-logs/${TODAY}.jsonl ($LOG_LINES entries)"
|
|
119
|
+
BLOCK="$BLOCK\n\n**POST-COMPACTION INSTRUCTIONS:**"
|
|
120
|
+
BLOCK="$BLOCK\n1. Call nexo_heartbeat with the SID above to reconnect with the session"
|
|
121
|
+
BLOCK="$BLOCK\n2. If you need specific lost data, query tool logs with jq"
|
|
122
|
+
BLOCK="$BLOCK\n3. Continue the task from where it left off — do NOT start from zero"
|
|
123
|
+
BLOCK="$BLOCK\n4. MCP tools (nexo_*) have all persistent state"
|
|
124
|
+
|
|
125
|
+
# Escape for JSON
|
|
126
|
+
BLOCK_ESCAPED=$(echo -e "$BLOCK" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
|
|
127
|
+
|
|
128
|
+
cat << HOOKEOF
|
|
129
|
+
{
|
|
130
|
+
"systemMessage": $BLOCK_ESCAPED
|
|
131
|
+
}
|
|
132
|
+
HOOKEOF
|
|
133
|
+
else
|
|
134
|
+
# No checkpoint — fallback to basic message
|
|
135
|
+
cat << 'HOOKEOF'
|
|
136
|
+
{
|
|
137
|
+
"systemMessage": "Post-compaction: no prior checkpoint found. Call nexo_heartbeat to reconnect session state."
|
|
138
|
+
}
|
|
139
|
+
HOOKEOF
|
|
140
|
+
fi
|
|
141
|
+
else
|
|
142
|
+
cat << 'HOOKEOF'
|
|
143
|
+
{
|
|
144
|
+
"systemMessage": "Post-compaction: nexo.db not found. Reconnect via nexo_startup."
|
|
145
|
+
}
|
|
146
|
+
HOOKEOF
|
|
147
|
+
fi
|
package/src/hooks/pre-compact.sh
CHANGED
|
@@ -1,65 +1,49 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# NEXO PreCompact
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# NEXO PreCompact Hook — Save checkpoint + inject preservation instructions
|
|
3
|
+
# This runs BEFORE Claude Code compacts. It:
|
|
4
|
+
# 1. Enriches the session checkpoint in SQLite with latest diary draft data
|
|
5
|
+
# 2. Injects a systemMessage telling the operator to save any WIP via MCP tools
|
|
5
6
|
set -euo pipefail
|
|
6
7
|
|
|
7
8
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
NEXO_DB="$NEXO_HOME/nexo.db"
|
|
10
|
+
TODAY=$(date +%Y-%m-%d)
|
|
11
|
+
LOG_FILE="$NEXO_HOME/operations/tool-logs/${TODAY}.jsonl"
|
|
12
|
+
LOG_LINES=0
|
|
13
|
+
if [ -f "$LOG_FILE" ]; then
|
|
14
|
+
LOG_LINES=$(wc -l < "$LOG_FILE" | tr -d ' ')
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Enrich checkpoint: copy diary draft context into checkpoint if exists
|
|
18
|
+
if [ -f "$NEXO_DB" ]; then
|
|
19
|
+
# Get latest active session's diary draft
|
|
20
|
+
LATEST_SID=$(sqlite3 "$NEXO_DB" "
|
|
21
|
+
SELECT sid FROM sessions ORDER BY last_update_epoch DESC LIMIT 1
|
|
22
|
+
" 2>/dev/null || echo "")
|
|
23
|
+
|
|
24
|
+
if [ -n "$LATEST_SID" ]; then
|
|
25
|
+
# Write SID to temp file so PostCompact knows which session compacted
|
|
26
|
+
echo "$LATEST_SID" > /tmp/nexo-compacting-sid
|
|
27
|
+
# Pull diary draft data into checkpoint
|
|
28
|
+
sqlite3 "$NEXO_DB" "
|
|
29
|
+
INSERT INTO session_checkpoints (sid, task, current_goal, updated_at)
|
|
30
|
+
SELECT s.sid, s.task, COALESCE(d.last_context_hint, s.task), datetime('now')
|
|
31
|
+
FROM sessions s
|
|
32
|
+
LEFT JOIN session_diary_draft d ON d.sid = s.sid
|
|
33
|
+
WHERE s.sid = '$LATEST_SID'
|
|
34
|
+
ON CONFLICT(sid) DO UPDATE SET
|
|
35
|
+
task = excluded.task,
|
|
36
|
+
current_goal = CASE
|
|
37
|
+
WHEN excluded.current_goal != '' THEN excluded.current_goal
|
|
38
|
+
ELSE session_checkpoints.current_goal
|
|
39
|
+
END,
|
|
40
|
+
updated_at = datetime('now')
|
|
41
|
+
" 2>/dev/null || true
|
|
42
|
+
fi
|
|
43
|
+
fi
|
|
10
44
|
|
|
11
|
-
mkdir -p "$NEXO_HOME/coordination"
|
|
12
|
-
|
|
13
|
-
# Save current state to checkpoint file
|
|
14
|
-
python3 -c "
|
|
15
|
-
import json, os, sys
|
|
16
|
-
from datetime import datetime
|
|
17
|
-
|
|
18
|
-
nexo_home = os.environ.get('NEXO_HOME', os.path.expanduser('~/.nexo'))
|
|
19
|
-
db_path = os.path.join(nexo_home, 'nexo.db')
|
|
20
|
-
|
|
21
|
-
checkpoint = {
|
|
22
|
-
'timestamp': datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S'),
|
|
23
|
-
'active_sessions': [],
|
|
24
|
-
'last_context_hints': [],
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
try:
|
|
28
|
-
import sqlite3
|
|
29
|
-
if os.path.exists(db_path):
|
|
30
|
-
db = sqlite3.connect(db_path, timeout=5)
|
|
31
|
-
db.row_factory = sqlite3.Row
|
|
32
|
-
sessions = db.execute(
|
|
33
|
-
'SELECT sid, task, started FROM sessions WHERE completed=0'
|
|
34
|
-
).fetchall()
|
|
35
|
-
checkpoint['active_sessions'] = [
|
|
36
|
-
{'sid': s['sid'], 'task': s['task'], 'started': s['started']}
|
|
37
|
-
for s in sessions
|
|
38
|
-
]
|
|
39
|
-
# Get last diary drafts for context
|
|
40
|
-
try:
|
|
41
|
-
drafts = db.execute(
|
|
42
|
-
'SELECT sid, last_context_hint, tasks_seen FROM session_diary_draft '
|
|
43
|
-
'ORDER BY updated_at DESC LIMIT 3'
|
|
44
|
-
).fetchall()
|
|
45
|
-
checkpoint['last_context_hints'] = [
|
|
46
|
-
{'sid': d['sid'], 'hint': d['last_context_hint'], 'tasks': d['tasks_seen']}
|
|
47
|
-
for d in drafts
|
|
48
|
-
]
|
|
49
|
-
except Exception:
|
|
50
|
-
pass
|
|
51
|
-
db.close()
|
|
52
|
-
except Exception:
|
|
53
|
-
pass
|
|
54
|
-
|
|
55
|
-
with open('$CHECKPOINT_FILE', 'w') as f:
|
|
56
|
-
json.dump(checkpoint, f, indent=2)
|
|
57
|
-
" 2>/dev/null || true
|
|
58
|
-
|
|
59
|
-
# Emit hook response with systemMessage
|
|
60
45
|
cat << HOOKEOF
|
|
61
46
|
{
|
|
62
|
-
"
|
|
63
|
-
"systemMessage": "PRE-COMPACT HOOK — Context is about to be compressed. BEFORE continuing:\n\n1. **Write a diary draft NOW** — call nexo_session_diary_write with what you've done so far, decisions made, and current mental state. This is your lifeline after compaction.\n2. **Note your current task** — after compaction you may lose the thread. Write it down in the diary.\n3. **Check pending followups** — if you promised to do something, make sure it's recorded before context is lost.\n4. **Read the checkpoint** after compaction: ${NEXO_HOME}/coordination/pre-compact-checkpoint.json\n\nDo NOT skip this. Compaction without a diary = starting from zero."
|
|
47
|
+
"systemMessage": "CONTEXT IS ABOUT TO BE COMPRESSED.\n\nOBLIGATORY ACTIONS BEFORE COMPACTION:\n1. Save critical state via MCP: nexo_checkpoint_save with current task, active files, decisions, errors, next step, and reasoning thread.\n2. If there is work in progress without a commit, save data via nexo_entity_create, nexo_preference_set, nexo_learning_add, nexo_followup_create.\n3. PERSISTENT TOOL LOGS: ${NEXO_HOME}/operations/tool-logs/${TODAY}.jsonl has ${LOG_LINES} entries.\n4. After compaction, the PostCompact hook will re-inject a Core Memory Block with the checkpoint.\n5. MCP tools (nexo_*) preserve all state — use them to recover context."
|
|
64
48
|
}
|
|
65
49
|
HOOKEOF
|
package/src/server.py
CHANGED
|
@@ -126,6 +126,77 @@ def nexo_smart_startup() -> str:
|
|
|
126
126
|
return handle_smart_startup_query()
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
# ── Session Checkpoints (auto-compaction continuity) ──────────────
|
|
130
|
+
|
|
131
|
+
@mcp.tool
|
|
132
|
+
def nexo_checkpoint_save(
|
|
133
|
+
sid: str,
|
|
134
|
+
task: str = '',
|
|
135
|
+
task_status: str = 'active',
|
|
136
|
+
active_files: str = '[]',
|
|
137
|
+
current_goal: str = '',
|
|
138
|
+
decisions_summary: str = '',
|
|
139
|
+
errors_found: str = '',
|
|
140
|
+
reasoning_thread: str = '',
|
|
141
|
+
next_step: str = ''
|
|
142
|
+
) -> str:
|
|
143
|
+
"""Save a session checkpoint for auto-compaction continuity.
|
|
144
|
+
|
|
145
|
+
Call this BEFORE context compaction to preserve session state.
|
|
146
|
+
The PostCompact hook reads this checkpoint and re-injects it as a
|
|
147
|
+
Core Memory Block, so the session continues seamlessly.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
sid: Session ID.
|
|
151
|
+
task: Current task description.
|
|
152
|
+
task_status: One of 'active', 'investigating', 'fixing', 'deploying', 'blocked'.
|
|
153
|
+
active_files: JSON array of file paths currently being worked on.
|
|
154
|
+
current_goal: What you're trying to achieve right now (1-2 sentences).
|
|
155
|
+
decisions_summary: Recent decisions with brief reasoning (2-3 lines).
|
|
156
|
+
errors_found: Errors encountered and their status (resolved/open).
|
|
157
|
+
reasoning_thread: Your current chain of thought (1-2 sentences).
|
|
158
|
+
next_step: The concrete next action to take.
|
|
159
|
+
"""
|
|
160
|
+
from db import save_checkpoint
|
|
161
|
+
result = save_checkpoint(
|
|
162
|
+
sid=sid, task=task, task_status=task_status,
|
|
163
|
+
active_files=active_files, current_goal=current_goal,
|
|
164
|
+
decisions_summary=decisions_summary, errors_found=errors_found,
|
|
165
|
+
reasoning_thread=reasoning_thread, next_step=next_step,
|
|
166
|
+
)
|
|
167
|
+
return f"Checkpoint saved for {sid}. Compaction #{result['compaction_count']}. PostCompact will re-inject this as Core Memory Block."
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@mcp.tool
|
|
171
|
+
def nexo_checkpoint_read(sid: str = '') -> str:
|
|
172
|
+
"""Read the latest session checkpoint. Used by PostCompact hook and for manual recovery.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
sid: Session ID. If empty, returns the most recent checkpoint from any session.
|
|
176
|
+
"""
|
|
177
|
+
from db import read_checkpoint
|
|
178
|
+
cp = read_checkpoint(sid)
|
|
179
|
+
if not cp:
|
|
180
|
+
return "No checkpoint found."
|
|
181
|
+
|
|
182
|
+
lines = [f"CHECKPOINT for {cp['sid']} (compaction #{cp['compaction_count']})"]
|
|
183
|
+
lines.append(f"Task: {cp['task']} ({cp['task_status']})")
|
|
184
|
+
if cp.get('current_goal'):
|
|
185
|
+
lines.append(f"Goal: {cp['current_goal']}")
|
|
186
|
+
if cp.get('active_files') and cp['active_files'] != '[]':
|
|
187
|
+
lines.append(f"Files: {cp['active_files']}")
|
|
188
|
+
if cp.get('decisions_summary'):
|
|
189
|
+
lines.append(f"Decisions: {cp['decisions_summary']}")
|
|
190
|
+
if cp.get('errors_found'):
|
|
191
|
+
lines.append(f"Errors: {cp['errors_found']}")
|
|
192
|
+
if cp.get('reasoning_thread'):
|
|
193
|
+
lines.append(f"Context: {cp['reasoning_thread']}")
|
|
194
|
+
if cp.get('next_step'):
|
|
195
|
+
lines.append(f"Next: {cp['next_step']}")
|
|
196
|
+
lines.append(f"Updated: {cp['updated_at']}")
|
|
197
|
+
return "\n".join(lines)
|
|
198
|
+
|
|
199
|
+
|
|
129
200
|
# ── File coordination (3 tools) ───────────────────────────────────
|
|
130
201
|
|
|
131
202
|
@mcp.tool
|
package/src/tools_sessions.py
CHANGED
|
@@ -8,6 +8,7 @@ from db import (
|
|
|
8
8
|
get_active_sessions, clean_stale_sessions, search_sessions,
|
|
9
9
|
get_inbox, get_pending_questions, now_epoch,
|
|
10
10
|
SESSION_STALE_SECONDS, check_session_has_diary,
|
|
11
|
+
save_checkpoint, read_checkpoint, increment_compaction_count,
|
|
11
12
|
)
|
|
12
13
|
|
|
13
14
|
# ── Session Keepalive ────────────────────────────────────────────────
|
|
@@ -317,6 +318,16 @@ def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
|
|
|
317
318
|
except Exception:
|
|
318
319
|
pass # Draft accumulation is best-effort, never block heartbeat
|
|
319
320
|
|
|
321
|
+
# Update session checkpoint with current goal (lightweight, every heartbeat)
|
|
322
|
+
try:
|
|
323
|
+
save_checkpoint(
|
|
324
|
+
sid=sid,
|
|
325
|
+
task=task,
|
|
326
|
+
current_goal=context_hint[:300] if context_hint else task,
|
|
327
|
+
)
|
|
328
|
+
except Exception:
|
|
329
|
+
pass # Checkpoint update is best-effort
|
|
330
|
+
|
|
320
331
|
# Diary reminder: after 30 min active with no diary entry
|
|
321
332
|
conn = get_db()
|
|
322
333
|
row = conn.execute("SELECT started_epoch FROM sessions WHERE sid = ?", (sid,)).fetchone()
|