nexo-brain 7.20.21 → 7.20.23
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 +3 -1
- package/package.json +1 -1
- package/src/auto_update.py +49 -19
- package/src/cli.py +21 -5
- package/src/db_guard.py +27 -9
- package/src/doctor/providers/boot.py +91 -2
- package/src/interactive_db.py +59 -0
- package/src/local_context/api.py +274 -81
- package/src/local_context/db.py +336 -0
- package/src/local_context/logging.py +3 -4
- package/src/mcp_required_tools.py +31 -0
- package/src/plugins/episodic_memory.py +18 -0
- package/src/plugins/recover.py +7 -4
- package/src/plugins/skills.py +14 -3
- package/src/plugins/update.py +37 -12
- package/src/scripts/nexo-backup.sh +131 -7
- package/src/server.py +97 -7
- package/src/tools_reminders.py +37 -8
- package/src/tools_sessions.py +11 -19
|
@@ -8,24 +8,130 @@ if [ ! -d "$BACKUP_DIR" ] && [ -d "$NEXO_HOME/backups" ]; then
|
|
|
8
8
|
fi
|
|
9
9
|
WEEKLY_DIR="$BACKUP_DIR/weekly"
|
|
10
10
|
DB="$NEXO_HOME/runtime/data/nexo.db"
|
|
11
|
-
|
|
11
|
+
LOCAL_CONTEXT_DB="$NEXO_HOME/runtime/memory/local-context.db"
|
|
12
|
+
LOCK_FILE="$NEXO_HOME/runtime/logs/local-index.lock"
|
|
13
|
+
RETENTION_HOURS="${NEXO_BACKUP_RETENTION_HOURS:-24}"
|
|
14
|
+
KEEP_LAST="${NEXO_BACKUP_KEEP_LAST:-3}"
|
|
15
|
+
FAMILY_KEEP_LAST="${NEXO_BACKUP_FAMILY_KEEP_LAST:-2}"
|
|
16
|
+
LOCAL_CONTEXT_RETENTION_HOURS="${NEXO_LOCAL_CONTEXT_BACKUP_RETENTION_HOURS:-24}"
|
|
17
|
+
LOCAL_CONTEXT_KEEP_LAST="${NEXO_LOCAL_CONTEXT_BACKUP_KEEP_LAST:-2}"
|
|
18
|
+
BUSY_TIMEOUT_MS="${NEXO_BACKUP_BUSY_TIMEOUT_MS:-5000}"
|
|
19
|
+
RECENT_BACKUP_HOURS="${NEXO_BACKUP_RECENT_BACKUP_HOURS:-6}"
|
|
12
20
|
|
|
13
21
|
mkdir -p "$BACKUP_DIR" "$WEEKLY_DIR"
|
|
14
22
|
|
|
23
|
+
cleanup_backups() {
|
|
24
|
+
python3 - "$BACKUP_DIR" "$RETENTION_HOURS" "$KEEP_LAST" "$FAMILY_KEEP_LAST" "$LOCAL_CONTEXT_RETENTION_HOURS" "$LOCAL_CONTEXT_KEEP_LAST" <<'PY'
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import shutil
|
|
28
|
+
import sys
|
|
29
|
+
import time
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
base = Path(sys.argv[1])
|
|
33
|
+
retention_hours = max(1, int(sys.argv[2]))
|
|
34
|
+
keep_last = max(1, int(sys.argv[3]))
|
|
35
|
+
family_keep_last = max(1, int(sys.argv[4]))
|
|
36
|
+
local_context_retention_hours = max(1, int(sys.argv[5]))
|
|
37
|
+
local_context_keep_last = max(1, int(sys.argv[6]))
|
|
38
|
+
now = time.time()
|
|
39
|
+
|
|
40
|
+
def delete_path(path: Path) -> None:
|
|
41
|
+
try:
|
|
42
|
+
if path.is_dir():
|
|
43
|
+
shutil.rmtree(path)
|
|
44
|
+
else:
|
|
45
|
+
path.unlink()
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
pass
|
|
48
|
+
except Exception as exc:
|
|
49
|
+
print(f"NEXO backup cleanup warning: {path}: {exc}", file=sys.stderr)
|
|
50
|
+
|
|
51
|
+
for tmp in base.glob("*.tmp.*"):
|
|
52
|
+
try:
|
|
53
|
+
if now - tmp.stat().st_mtime > 1800:
|
|
54
|
+
delete_path(tmp)
|
|
55
|
+
except FileNotFoundError:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
hourlies = sorted(base.glob("nexo-*.db"), key=lambda p: p.stat().st_mtime if p.exists() else 0, reverse=True)
|
|
59
|
+
for backup in hourlies[keep_last:]:
|
|
60
|
+
try:
|
|
61
|
+
age_hours = (now - backup.stat().st_mtime) / 3600
|
|
62
|
+
except FileNotFoundError:
|
|
63
|
+
continue
|
|
64
|
+
if age_hours > retention_hours:
|
|
65
|
+
delete_path(backup)
|
|
66
|
+
|
|
67
|
+
local_context_hourlies = sorted(
|
|
68
|
+
base.glob("local-context-*.db"),
|
|
69
|
+
key=lambda p: p.stat().st_mtime if p.exists() else 0,
|
|
70
|
+
reverse=True,
|
|
71
|
+
)
|
|
72
|
+
for backup in local_context_hourlies[local_context_keep_last:]:
|
|
73
|
+
try:
|
|
74
|
+
age_hours = (now - backup.stat().st_mtime) / 3600
|
|
75
|
+
except FileNotFoundError:
|
|
76
|
+
continue
|
|
77
|
+
if age_hours > local_context_retention_hours:
|
|
78
|
+
delete_path(backup)
|
|
79
|
+
|
|
80
|
+
for pattern in (
|
|
81
|
+
"pre-backfill-owner-*",
|
|
82
|
+
"pre-update-*",
|
|
83
|
+
"pre-autoupdate-*",
|
|
84
|
+
"pre-restore-*",
|
|
85
|
+
"app-reinstall-*",
|
|
86
|
+
"app-install-*",
|
|
87
|
+
"desktop-local-install-*",
|
|
88
|
+
"code-tree-*",
|
|
89
|
+
):
|
|
90
|
+
entries = sorted(base.glob(pattern), key=lambda p: p.stat().st_mtime if p.exists() else 0, reverse=True)
|
|
91
|
+
for entry in entries[family_keep_last:]:
|
|
92
|
+
delete_path(entry)
|
|
93
|
+
|
|
94
|
+
weekly = base / "weekly"
|
|
95
|
+
if weekly.exists():
|
|
96
|
+
for backup in weekly.glob("weekly-*.db"):
|
|
97
|
+
try:
|
|
98
|
+
if now - backup.stat().st_mtime > 90 * 24 * 3600:
|
|
99
|
+
delete_path(backup)
|
|
100
|
+
except FileNotFoundError:
|
|
101
|
+
pass
|
|
102
|
+
PY
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
has_recent_backup() {
|
|
106
|
+
find "$BACKUP_DIR" -maxdepth 1 -name "nexo-*.db" -mmin "-$((RECENT_BACKUP_HOURS * 60))" -print -quit | grep -q .
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
has_recent_local_context_backup() {
|
|
110
|
+
find "$BACKUP_DIR" -maxdepth 1 -name "local-context-*.db" -mmin "-$((RECENT_BACKUP_HOURS * 60))" -print -quit | grep -q .
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
cleanup_backups
|
|
114
|
+
|
|
15
115
|
# Hourly backup
|
|
16
116
|
TIMESTAMP=$(date +%Y-%m-%d-%H%M)
|
|
17
117
|
BACKUP_FILE="$BACKUP_DIR/nexo-$TIMESTAMP.db"
|
|
18
118
|
TMP_BACKUP="$BACKUP_FILE.tmp.$$"
|
|
19
119
|
rm -f "$TMP_BACKUP"
|
|
20
|
-
if sqlite3 -cmd ".timeout
|
|
21
|
-
PRAGMA busy_timeout
|
|
120
|
+
if sqlite3 -cmd ".timeout $BUSY_TIMEOUT_MS" "$DB" <<SQL
|
|
121
|
+
PRAGMA busy_timeout=$BUSY_TIMEOUT_MS;
|
|
22
122
|
.backup '$TMP_BACKUP'
|
|
23
123
|
SQL
|
|
24
124
|
then
|
|
25
125
|
mv "$TMP_BACKUP" "$BACKUP_FILE"
|
|
26
126
|
else
|
|
27
127
|
rm -f "$TMP_BACKUP"
|
|
28
|
-
|
|
128
|
+
if has_recent_backup; then
|
|
129
|
+
echo "NEXO backup skipped: database busy and a recent backup exists" >&2
|
|
130
|
+
cleanup_backups
|
|
131
|
+
exit 0
|
|
132
|
+
fi
|
|
133
|
+
echo "NEXO backup failed: database busy or unavailable and no recent backup exists" >&2
|
|
134
|
+
cleanup_backups
|
|
29
135
|
exit 1
|
|
30
136
|
fi
|
|
31
137
|
|
|
@@ -36,6 +142,24 @@ if [ ! -f "$WEEKLY_FILE" ] && [ "$(date +%u)" = "7" ] && [ -f "$BACKUP_FILE" ];
|
|
|
36
142
|
cp "$BACKUP_FILE" "$WEEKLY_FILE"
|
|
37
143
|
fi
|
|
38
144
|
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
145
|
+
# Local memory backup: separate and aggressively rotated so the index cannot
|
|
146
|
+
# block core DB backups or fill the disk with duplicate multi-GB snapshots.
|
|
147
|
+
if [ -f "$LOCAL_CONTEXT_DB" ]; then
|
|
148
|
+
LOCAL_CONTEXT_BACKUP_FILE="$BACKUP_DIR/local-context-$TIMESTAMP.db"
|
|
149
|
+
LOCAL_CONTEXT_TMP_BACKUP="$LOCAL_CONTEXT_BACKUP_FILE.tmp.$$"
|
|
150
|
+
rm -f "$LOCAL_CONTEXT_TMP_BACKUP"
|
|
151
|
+
if [ -f "$LOCK_FILE" ] && find "$LOCK_FILE" -mmin -30 -print -quit | grep -q . && has_recent_local_context_backup; then
|
|
152
|
+
echo "NEXO local memory backup skipped: index is active and a recent local backup exists"
|
|
153
|
+
elif sqlite3 -cmd ".timeout $BUSY_TIMEOUT_MS" "$LOCAL_CONTEXT_DB" <<SQL
|
|
154
|
+
PRAGMA busy_timeout=$BUSY_TIMEOUT_MS;
|
|
155
|
+
.backup '$LOCAL_CONTEXT_TMP_BACKUP'
|
|
156
|
+
SQL
|
|
157
|
+
then
|
|
158
|
+
mv "$LOCAL_CONTEXT_TMP_BACKUP" "$LOCAL_CONTEXT_BACKUP_FILE"
|
|
159
|
+
else
|
|
160
|
+
rm -f "$LOCAL_CONTEXT_TMP_BACKUP"
|
|
161
|
+
echo "NEXO local memory backup warning: local-context database busy or unavailable" >&2
|
|
162
|
+
fi
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
cleanup_backups
|
package/src/server.py
CHANGED
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import signal
|
|
6
6
|
import sys
|
|
7
7
|
import json
|
|
8
|
+
import time
|
|
8
9
|
|
|
9
10
|
from fastmcp import FastMCP
|
|
10
11
|
from core_prompts import render_core_prompt
|
|
@@ -94,10 +95,15 @@ from tools_automation_sessions import (
|
|
|
94
95
|
from plugins.cortex import handle_cortex_check
|
|
95
96
|
from plugins.guard import handle_guard_check
|
|
96
97
|
from plugins.protocol import (
|
|
98
|
+
handle_confidence_check,
|
|
99
|
+
handle_protocol_debt_resolve,
|
|
97
100
|
handle_task_acknowledge_guard,
|
|
98
101
|
handle_task_close,
|
|
99
102
|
handle_task_open,
|
|
100
103
|
)
|
|
104
|
+
from plugins.episodic_memory import handle_session_diary_read
|
|
105
|
+
from plugins.cards import handle_card_match
|
|
106
|
+
from plugins.skills import handle_skill_match
|
|
101
107
|
from plugins.workflow import (
|
|
102
108
|
handle_workflow_open,
|
|
103
109
|
handle_workflow_update,
|
|
@@ -112,10 +118,12 @@ from runtime_versioning import (
|
|
|
112
118
|
prime_process_version,
|
|
113
119
|
)
|
|
114
120
|
from local_context import api as local_context_api
|
|
121
|
+
from local_context.db import close_local_context_db
|
|
115
122
|
|
|
116
123
|
|
|
117
124
|
# ── Graceful shutdown: close DB on any termination signal ──────────
|
|
118
125
|
def _shutdown_handler(signum, frame):
|
|
126
|
+
close_local_context_db()
|
|
119
127
|
close_db()
|
|
120
128
|
sys.exit(0)
|
|
121
129
|
|
|
@@ -184,11 +192,23 @@ def _restore_valid_db_backup() -> bool:
|
|
|
184
192
|
def _init_db_or_exit() -> None:
|
|
185
193
|
import sqlite3
|
|
186
194
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
for attempt in range(3):
|
|
196
|
+
try:
|
|
197
|
+
init_db()
|
|
198
|
+
return
|
|
199
|
+
except sqlite3.OperationalError as exc:
|
|
200
|
+
message = str(exc).lower()
|
|
201
|
+
if "locked" in message or "busy" in message:
|
|
202
|
+
if attempt < 2:
|
|
203
|
+
time.sleep(0.25 * (attempt + 1))
|
|
204
|
+
continue
|
|
205
|
+
print(f"[NEXO] DB init temporarily busy: {exc}", file=sys.stderr)
|
|
206
|
+
raise SystemExit(75)
|
|
207
|
+
print(f"[NEXO] DB init failed: {exc}", file=sys.stderr)
|
|
208
|
+
break
|
|
209
|
+
except sqlite3.DatabaseError as exc:
|
|
210
|
+
print(f"[NEXO] DB init failed: {exc}", file=sys.stderr)
|
|
211
|
+
break
|
|
192
212
|
|
|
193
213
|
restored = False
|
|
194
214
|
try:
|
|
@@ -439,6 +459,76 @@ def nexo_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
|
|
|
439
459
|
return handle_heartbeat(sid, task, context_hint)
|
|
440
460
|
|
|
441
461
|
|
|
462
|
+
@mcp.tool
|
|
463
|
+
def nexo_session_diary_read(
|
|
464
|
+
session_id: str = "",
|
|
465
|
+
last_n: int = 3,
|
|
466
|
+
last_day: bool = False,
|
|
467
|
+
domain: str = "",
|
|
468
|
+
brief: bool = False,
|
|
469
|
+
) -> str:
|
|
470
|
+
"""Read recent session diaries for context continuity."""
|
|
471
|
+
return handle_session_diary_read(session_id, last_n, last_day, domain, brief)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@mcp.tool
|
|
475
|
+
def nexo_confidence_check(
|
|
476
|
+
goal: str,
|
|
477
|
+
task_type: str = "answer",
|
|
478
|
+
area: str = "",
|
|
479
|
+
context_hint: str = "",
|
|
480
|
+
constraints: str = "[]",
|
|
481
|
+
evidence_refs: str = "[]",
|
|
482
|
+
unknowns: str = "[]",
|
|
483
|
+
verification_step: str = "",
|
|
484
|
+
stakes: str = "",
|
|
485
|
+
) -> str:
|
|
486
|
+
"""Decide whether an answer should proceed directly or be verified first."""
|
|
487
|
+
return handle_confidence_check(
|
|
488
|
+
goal,
|
|
489
|
+
task_type,
|
|
490
|
+
area,
|
|
491
|
+
context_hint,
|
|
492
|
+
constraints,
|
|
493
|
+
evidence_refs,
|
|
494
|
+
unknowns,
|
|
495
|
+
verification_step,
|
|
496
|
+
stakes,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@mcp.tool
|
|
501
|
+
def nexo_protocol_debt_resolve(
|
|
502
|
+
debt_ids: str = "",
|
|
503
|
+
task_id: str = "",
|
|
504
|
+
session_id: str = "",
|
|
505
|
+
debt_types: str = "",
|
|
506
|
+
resolution: str = "",
|
|
507
|
+
debt_id: str = "",
|
|
508
|
+
) -> str:
|
|
509
|
+
"""Resolve protocol debt records by id or filters."""
|
|
510
|
+
return handle_protocol_debt_resolve(debt_ids, task_id, session_id, debt_types, resolution, debt_id)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@mcp.tool
|
|
514
|
+
def nexo_card_match(
|
|
515
|
+
query: str,
|
|
516
|
+
limit: int = 5,
|
|
517
|
+
include_protocol: bool = True,
|
|
518
|
+
locale: str = "es",
|
|
519
|
+
category: str = "",
|
|
520
|
+
business_type: str = "",
|
|
521
|
+
) -> str:
|
|
522
|
+
"""Find official NEXO protocol cards for a user request."""
|
|
523
|
+
return handle_card_match(query, limit, include_protocol, locale, category, business_type)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
@mcp.tool
|
|
527
|
+
def nexo_skill_match(task: str, level: str = "") -> str:
|
|
528
|
+
"""Find reusable NEXO skills for the current task."""
|
|
529
|
+
return handle_skill_match(task, level)
|
|
530
|
+
|
|
531
|
+
|
|
442
532
|
@mcp.tool
|
|
443
533
|
def nexo_stop(sid: str) -> str:
|
|
444
534
|
"""Cleanly close a session. Removes it from active sessions immediately.
|
|
@@ -720,7 +810,7 @@ def nexo_local_index_roots(action: str = "list", path: str = "", mode: str = "no
|
|
|
720
810
|
"""List, add or remove local memory roots."""
|
|
721
811
|
normalized = str(action or "list").strip().lower()
|
|
722
812
|
if normalized == "list":
|
|
723
|
-
result = {"ok": True, "roots": local_context_api.list_roots()}
|
|
813
|
+
result = {"ok": True, "roots": local_context_api.list_roots(readonly=True)}
|
|
724
814
|
elif normalized == "add":
|
|
725
815
|
result = local_context_api.add_root(path, mode=mode, depth=depth)
|
|
726
816
|
elif normalized == "remove":
|
|
@@ -735,7 +825,7 @@ def nexo_local_index_exclusions(action: str = "list", path: str = "", reason: st
|
|
|
735
825
|
"""List, add or remove local memory exclusions."""
|
|
736
826
|
normalized = str(action or "list").strip().lower()
|
|
737
827
|
if normalized == "list":
|
|
738
|
-
result = {"ok": True, "exclusions": local_context_api.list_exclusions()}
|
|
828
|
+
result = {"ok": True, "exclusions": local_context_api.list_exclusions(readonly=True)}
|
|
739
829
|
elif normalized == "add":
|
|
740
830
|
result = local_context_api.add_exclusion(path, reason=reason)
|
|
741
831
|
elif normalized == "remove":
|
package/src/tools_reminders.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from db import get_reminders, get_followups
|
|
4
4
|
from datetime import date
|
|
5
|
+
from interactive_db import interactive_db_timeout, is_db_busy
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def _is_due(date_str: str) -> bool:
|
|
@@ -22,21 +23,49 @@ def handle_reminders(filter_type: str = "due") -> str:
|
|
|
22
23
|
filter_type: 'due', 'all', 'followups', 'completed', 'deleted', 'history', 'any'
|
|
23
24
|
"""
|
|
24
25
|
parts = []
|
|
26
|
+
warnings: list[str] = []
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
with interactive_db_timeout():
|
|
29
|
+
if filter_type in ("due", "all", "completed", "deleted", "history", "any"):
|
|
30
|
+
r = _format_reminders_safe(filter_type, warnings)
|
|
31
|
+
if r:
|
|
32
|
+
parts.append(r)
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
if filter_type in ("due", "all", "followups", "completed", "deleted", "history", "any"):
|
|
35
|
+
f = _format_followups_safe(filter_type, warnings)
|
|
36
|
+
if f:
|
|
37
|
+
parts.append(f)
|
|
35
38
|
|
|
36
39
|
result = "\n\n".join(parts)
|
|
40
|
+
if warnings:
|
|
41
|
+
prefix = "REMINDERS DEGRADED:\n " + "\n ".join(warnings)
|
|
42
|
+
prefix += "\n Continue with the user request; reminders will catch up shortly."
|
|
43
|
+
return f"{prefix}\n\n{result}" if result else prefix
|
|
37
44
|
return result if result else "No pending reminders."
|
|
38
45
|
|
|
39
46
|
|
|
47
|
+
def _format_reminders_safe(filter_type: str, warnings: list[str]) -> str:
|
|
48
|
+
try:
|
|
49
|
+
return _format_reminders(filter_type)
|
|
50
|
+
except Exception as exc:
|
|
51
|
+
if is_db_busy(exc):
|
|
52
|
+
warnings.append("reminders skipped because the local brain database is busy")
|
|
53
|
+
else:
|
|
54
|
+
warnings.append(f"reminders skipped ({type(exc).__name__})")
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _format_followups_safe(filter_type: str, warnings: list[str]) -> str:
|
|
59
|
+
try:
|
|
60
|
+
return _format_followups(filter_type)
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
if is_db_busy(exc):
|
|
63
|
+
warnings.append("followups skipped because the local brain database is busy")
|
|
64
|
+
else:
|
|
65
|
+
warnings.append(f"followups skipped ({type(exc).__name__})")
|
|
66
|
+
return ""
|
|
67
|
+
|
|
68
|
+
|
|
40
69
|
def _format_reminders(filter_type: str) -> str:
|
|
41
70
|
"""Format reminders from database."""
|
|
42
71
|
rows = get_reminders(filter_type)
|
package/src/tools_sessions.py
CHANGED
|
@@ -425,17 +425,6 @@ def handle_startup(
|
|
|
425
425
|
sid = _generate_sid()
|
|
426
426
|
startup_warnings: list[str] = []
|
|
427
427
|
cleaned = _safe_interactive("stale-session cleanup", clean_stale_sessions, 0, startup_warnings)
|
|
428
|
-
if startup_warnings:
|
|
429
|
-
lines = [f"SID: {sid}"]
|
|
430
|
-
conversation = str(conversation_id or "").strip()
|
|
431
|
-
if conversation:
|
|
432
|
-
lines.append(f"CONVERSATION_ID: {conversation}")
|
|
433
|
-
lines.append("")
|
|
434
|
-
lines.append("STARTUP DEGRADED:")
|
|
435
|
-
for warning in startup_warnings[:4]:
|
|
436
|
-
lines.append(f" {warning}")
|
|
437
|
-
lines.append(" Continue responding; retry nexo_heartbeat shortly for full context.")
|
|
438
|
-
return "\n".join(lines)
|
|
439
428
|
linked_session_id = (session_token or claude_session_id or "").strip()
|
|
440
429
|
# v6.0.7 hotfix: when the caller did not pass an explicit UUID, fall back to
|
|
441
430
|
# the Claude Code SessionStart UUID written by the SessionStart hook to
|
|
@@ -485,9 +474,12 @@ def handle_startup(
|
|
|
485
474
|
startup_warnings,
|
|
486
475
|
)
|
|
487
476
|
memory_maintenance = None
|
|
488
|
-
|
|
477
|
+
try:
|
|
478
|
+
backfill_limit = int(os.environ.get("NEXO_MEMORY_STARTUP_BACKFILL_LIMIT", "0") or "0")
|
|
479
|
+
except Exception:
|
|
480
|
+
backfill_limit = 0
|
|
481
|
+
if _env_flag("NEXO_MEMORY_MAINTENANCE_IN_STARTUP", default=False) or backfill_limit > 0:
|
|
489
482
|
try:
|
|
490
|
-
backfill_limit = int(os.environ.get("NEXO_MEMORY_STARTUP_BACKFILL_LIMIT", "0") or "0")
|
|
491
483
|
memory_maintenance = maintain_memory_observations(
|
|
492
484
|
process_limit=int(os.environ.get("NEXO_MEMORY_STARTUP_PROCESS_LIMIT", "20") or "20"),
|
|
493
485
|
retry_failed=True,
|
|
@@ -794,7 +786,7 @@ def _handle_heartbeat_inner(sid: str, task: str, context_hint: str = '') -> str:
|
|
|
794
786
|
if bundle.get("has_matches"):
|
|
795
787
|
parts.append("")
|
|
796
788
|
parts.append(format_pre_action_context_bundle(bundle, compact=True))
|
|
797
|
-
if _env_flag("NEXO_HEARTBEAT_LOCAL_CONTEXT", default=
|
|
789
|
+
if _env_flag("NEXO_HEARTBEAT_LOCAL_CONTEXT", default=True) and append_local_context_evidence is not None:
|
|
798
790
|
local_rendered = append_local_context_evidence("", recent_query, limit=4).strip()
|
|
799
791
|
if local_rendered:
|
|
800
792
|
parts.append("")
|
|
@@ -895,7 +887,7 @@ def _handle_heartbeat_inner(sid: str, task: str, context_hint: str = '') -> str:
|
|
|
895
887
|
|
|
896
888
|
# ── Drive/Curiosity: detect signals from context_hint (best-effort) ──
|
|
897
889
|
try:
|
|
898
|
-
if _env_flag("NEXO_DRIVE_IN_HEARTBEAT", default=
|
|
890
|
+
if _env_flag("NEXO_DRIVE_IN_HEARTBEAT", default=True) and context_hint and len(context_hint.strip()) >= 15:
|
|
899
891
|
from tools_drive import detect_drive_signal as _detect_drive
|
|
900
892
|
_drive_allow_llm = _env_flag("NEXO_DRIVE_LLM_IN_HEARTBEAT", default=False)
|
|
901
893
|
_drive_result = _detect_drive(
|
|
@@ -1001,7 +993,7 @@ def _handle_heartbeat_inner(sid: str, task: str, context_hint: str = '') -> str:
|
|
|
1001
993
|
# adaptive_log row per heartbeat. Wrapped in best-effort try/except so
|
|
1002
994
|
# a failure here cannot block the heartbeat itself.
|
|
1003
995
|
try:
|
|
1004
|
-
if _env_flag("NEXO_HEARTBEAT_ADAPTIVE_MODE", default=
|
|
996
|
+
if _env_flag("NEXO_HEARTBEAT_ADAPTIVE_MODE", default=True) and context_hint and len(context_hint.strip()) >= 5:
|
|
1005
997
|
from plugins.adaptive_mode import compute_mode
|
|
1006
998
|
from cognitive._trust import detect_sentiment
|
|
1007
999
|
sentiment = detect_sentiment(context_hint)
|
|
@@ -1345,9 +1337,7 @@ def _hint_suggests_correction(hint: str, *, correction_detector=None) -> bool:
|
|
|
1345
1337
|
text = (hint or "").strip()
|
|
1346
1338
|
if not text:
|
|
1347
1339
|
return False
|
|
1348
|
-
detector = correction_detector if correction_detector is not None else
|
|
1349
|
-
_detect_correction_semantic if _env_flag("NEXO_HEARTBEAT_SEMANTIC_DETECTORS", default=False) else None
|
|
1350
|
-
)
|
|
1340
|
+
detector = correction_detector if correction_detector is not None else _detect_correction_semantic
|
|
1351
1341
|
if detector is not None:
|
|
1352
1342
|
try:
|
|
1353
1343
|
if bool(detector(text)):
|
|
@@ -1386,6 +1376,8 @@ def _hint_suggests_correction(hint: str, *, correction_detector=None) -> bool:
|
|
|
1386
1376
|
|
|
1387
1377
|
def _recent_learning_capture_exists(conn, sid: str, window_seconds: int = 300) -> bool:
|
|
1388
1378
|
"""Check whether a recent learning was captured manually or via protocol task close."""
|
|
1379
|
+
if conn is None:
|
|
1380
|
+
conn = get_db()
|
|
1389
1381
|
cutoff_epoch = time.time() - window_seconds
|
|
1390
1382
|
|
|
1391
1383
|
row = conn.execute(
|