nexo-brain 3.0.1 → 3.1.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 +82 -0
- package/community/launch/2026-04-v3-0-2/case-study-outreach.md +36 -0
- package/community/launch/2026-04-v3-0-2/devto-v3-0-2.md +91 -0
- package/community/launch/2026-04-v3-0-2/github-discussion-v3-0-2.md +58 -0
- package/community/launch/2026-04-v3-0-2/x-thread-v3-0-2.md +60 -0
- package/hooks/hooks.json +12 -0
- package/package.json +1 -1
- package/src/auto_update.py +23 -6
- package/src/client_sync.py +6 -0
- package/src/cognitive/_memory.py +14 -7
- package/src/cognitive/_search.py +12 -5
- package/src/crons/sync.py +2 -1
- package/src/doctor/models.py +25 -0
- package/src/doctor/orchestrator.py +32 -2
- package/src/doctor/providers/boot.py +7 -7
- package/src/doctor/providers/deep.py +24 -21
- package/src/doctor/providers/runtime.py +154 -137
- package/src/evolution_cycle.py +76 -47
- package/src/hook_guardrails.py +182 -0
- package/src/hooks/protocol-guardrail.sh +1 -1
- package/src/hooks/protocol-pretool-guardrail.sh +9 -0
- package/src/kg_populate.py +21 -19
- package/src/maintenance.py +3 -3
- package/src/migrate_embeddings.py +36 -34
- package/src/plugins/backup.py +24 -12
- package/src/plugins/protocol.py +15 -0
- package/src/plugins/schedule.py +13 -1
- package/src/plugins/update.py +18 -4
- package/src/protocol_settings.py +59 -0
- package/src/public_contribution.py +10 -14
- package/src/public_evolution_queue.py +241 -0
- package/src/scripts/nexo-catchup.py +15 -15
- package/src/scripts/nexo-daily-self-audit.py +677 -28
- package/src/scripts/nexo-evolution-run.py +44 -4
- package/src/server.py +26 -1
- package/src/state_watchers_runtime.py +42 -35
package/src/evolution_cycle.py
CHANGED
|
@@ -120,52 +120,54 @@ def save_objective(obj: dict):
|
|
|
120
120
|
def get_week_data(db_path: str) -> dict:
|
|
121
121
|
"""Gather last 7 days of learnings, decisions, changes, diaries."""
|
|
122
122
|
conn = sqlite3.connect(db_path, timeout=10)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
123
|
+
try:
|
|
124
|
+
conn.row_factory = sqlite3.Row
|
|
125
|
+
cutoff_epoch = time.time() - 7 * 86400
|
|
126
|
+
cutoff_date = (date.today() - timedelta(days=7)).isoformat()
|
|
127
|
+
|
|
128
|
+
data = {}
|
|
129
|
+
|
|
130
|
+
rows = conn.execute(
|
|
131
|
+
"SELECT category, title, content FROM learnings WHERE created_at > ? ORDER BY created_at DESC LIMIT 50",
|
|
132
|
+
(cutoff_epoch,)
|
|
133
|
+
).fetchall()
|
|
134
|
+
data["learnings"] = [dict(r) for r in rows]
|
|
135
|
+
|
|
136
|
+
rows = conn.execute(
|
|
137
|
+
"SELECT domain, decision, alternatives, based_on, confidence, outcome FROM decisions "
|
|
138
|
+
"WHERE created_at > ? ORDER BY created_at DESC LIMIT 20",
|
|
139
|
+
(cutoff_date,)
|
|
140
|
+
).fetchall()
|
|
141
|
+
data["decisions"] = [dict(r) for r in rows]
|
|
142
|
+
|
|
143
|
+
rows = conn.execute(
|
|
144
|
+
"SELECT files, what_changed, why, affects, risks FROM change_log "
|
|
145
|
+
"WHERE created_at > ? ORDER BY created_at DESC LIMIT 30",
|
|
146
|
+
(cutoff_date,)
|
|
147
|
+
).fetchall()
|
|
148
|
+
data["changes"] = [dict(r) for r in rows]
|
|
149
|
+
|
|
150
|
+
rows = conn.execute(
|
|
151
|
+
"SELECT summary, decisions as diary_decisions, pending, mental_state, domain, user_signals "
|
|
152
|
+
"FROM session_diary WHERE created_at > ? ORDER BY created_at DESC LIMIT 20",
|
|
153
|
+
(cutoff_date,)
|
|
154
|
+
).fetchall()
|
|
155
|
+
data["diaries"] = [dict(r) for r in rows]
|
|
156
|
+
|
|
157
|
+
rows = conn.execute(
|
|
158
|
+
"SELECT * FROM evolution_log ORDER BY id DESC LIMIT 20"
|
|
159
|
+
).fetchall()
|
|
160
|
+
data["evolution_history"] = [dict(r) for r in rows]
|
|
161
|
+
|
|
162
|
+
rows = conn.execute(
|
|
163
|
+
"SELECT dimension, score, delta, measured_at FROM evolution_metrics "
|
|
164
|
+
"WHERE id IN (SELECT MAX(id) FROM evolution_metrics GROUP BY dimension)"
|
|
165
|
+
).fetchall()
|
|
166
|
+
data["current_metrics"] = {r["dimension"]: dict(r) for r in rows}
|
|
167
|
+
|
|
168
|
+
return data
|
|
169
|
+
finally:
|
|
170
|
+
conn.close()
|
|
169
171
|
|
|
170
172
|
|
|
171
173
|
def create_snapshot(files_to_backup: list) -> str:
|
|
@@ -348,7 +350,12 @@ Max 3 proposals. Quality over quantity. If nothing needs improving, say so."""
|
|
|
348
350
|
return prompt
|
|
349
351
|
|
|
350
352
|
|
|
351
|
-
def build_public_contribution_prompt(
|
|
353
|
+
def build_public_contribution_prompt(
|
|
354
|
+
*,
|
|
355
|
+
repo_root: str,
|
|
356
|
+
cycle_number: int,
|
|
357
|
+
queued_candidate: dict | None = None,
|
|
358
|
+
) -> str:
|
|
352
359
|
"""Prompt for the public-core contributor mode.
|
|
353
360
|
|
|
354
361
|
This prompt must never rely on private runtime state. It should inspect only
|
|
@@ -356,6 +363,27 @@ def build_public_contribution_prompt(*, repo_root: str, cycle_number: int) -> st
|
|
|
356
363
|
by returning machine-readable summary JSON.
|
|
357
364
|
"""
|
|
358
365
|
|
|
366
|
+
queued_section = ""
|
|
367
|
+
if queued_candidate:
|
|
368
|
+
queued_files = "\n".join(
|
|
369
|
+
f"- {path}" for path in (queued_candidate.get("files_changed") or [])[:20]
|
|
370
|
+
) or "- (no files recorded)"
|
|
371
|
+
queued_source = str((queued_candidate.get("metadata") or {}).get("source") or "managed-runtime")
|
|
372
|
+
queued_section = f"""
|
|
373
|
+
|
|
374
|
+
PRIORITY PUBLIC-PORT QUEUE ITEM:
|
|
375
|
+
- Source: {queued_source}
|
|
376
|
+
- Title: {str(queued_candidate.get("title") or "").strip()}
|
|
377
|
+
- Why it matters: {str(queued_candidate.get("reasoning") or "").strip()}
|
|
378
|
+
- Files originally touched:
|
|
379
|
+
{queued_files}
|
|
380
|
+
|
|
381
|
+
This item was already fixed or detected outside the public contribution runner.
|
|
382
|
+
Before inventing another improvement, verify whether the public repository still
|
|
383
|
+
needs the same change and port it if necessary. If the repo is already correct,
|
|
384
|
+
make the smallest validating change that captures the same gap.
|
|
385
|
+
"""
|
|
386
|
+
|
|
359
387
|
return f"""You are NEXO Public Evolution.
|
|
360
388
|
|
|
361
389
|
You are running inside an isolated checkout of the public NEXO repository.
|
|
@@ -387,6 +415,7 @@ What to do:
|
|
|
387
415
|
|
|
388
416
|
Cycle: #{cycle_number}
|
|
389
417
|
Quality over quantity. One strong improvement is better than three weak ones.
|
|
418
|
+
{queued_section}
|
|
390
419
|
"""
|
|
391
420
|
|
|
392
421
|
|
package/src/hook_guardrails.py
CHANGED
|
@@ -3,11 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
"""Post-tool guardrails for conditioned file learnings."""
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
from db import create_protocol_debt, get_db
|
|
10
11
|
from plugins.guard import _load_conditioned_learnings, _normalize_path_token
|
|
12
|
+
from protocol_settings import get_protocol_strictness
|
|
11
13
|
|
|
12
14
|
READ_LIKE_TOOLS = {"Read"}
|
|
13
15
|
WRITE_LIKE_TOOLS = {"Edit", "MultiEdit", "Write"}
|
|
@@ -94,6 +96,18 @@ def _find_open_task_for_file(conn, sid: str, filepath: str) -> dict | None:
|
|
|
94
96
|
return None
|
|
95
97
|
|
|
96
98
|
|
|
99
|
+
def _find_any_open_task(conn, sid: str) -> dict | None:
|
|
100
|
+
row = conn.execute(
|
|
101
|
+
"""SELECT task_id, files, guard_has_blocking
|
|
102
|
+
FROM protocol_tasks
|
|
103
|
+
WHERE session_id = ? AND status = 'open'
|
|
104
|
+
ORDER BY opened_at DESC
|
|
105
|
+
LIMIT 1""",
|
|
106
|
+
(sid,),
|
|
107
|
+
).fetchone()
|
|
108
|
+
return dict(row) if row else None
|
|
109
|
+
|
|
110
|
+
|
|
97
111
|
def _find_open_debt(conn, *, session_id: str, task_id: str, debt_type: str, file_token: str) -> dict | None:
|
|
98
112
|
row = conn.execute(
|
|
99
113
|
"""SELECT *
|
|
@@ -152,6 +166,136 @@ def _ensure_protocol_debt(
|
|
|
152
166
|
)
|
|
153
167
|
|
|
154
168
|
|
|
169
|
+
def process_pre_tool_event(payload: dict) -> dict:
|
|
170
|
+
strictness = get_protocol_strictness()
|
|
171
|
+
tool_name = str(payload.get("tool_name", "")).strip()
|
|
172
|
+
op = _operation_kind(tool_name)
|
|
173
|
+
if strictness == "lenient":
|
|
174
|
+
return {"ok": True, "skipped": True, "reason": "lenient mode", "strictness": strictness}
|
|
175
|
+
if op not in {"write", "delete"}:
|
|
176
|
+
return {"ok": True, "skipped": True, "reason": "operation not blocked", "strictness": strictness}
|
|
177
|
+
|
|
178
|
+
tool_input = payload.get("tool_input")
|
|
179
|
+
files = _extract_touched_files(tool_input)
|
|
180
|
+
conn = get_db()
|
|
181
|
+
sid = _resolve_nexo_sid(conn, str(payload.get("session_id", "")))
|
|
182
|
+
blocks: list[dict] = []
|
|
183
|
+
|
|
184
|
+
if not sid:
|
|
185
|
+
debt = _ensure_protocol_debt(
|
|
186
|
+
conn,
|
|
187
|
+
session_id="",
|
|
188
|
+
task_id="",
|
|
189
|
+
debt_type="strict_protocol_write_without_startup",
|
|
190
|
+
severity="error",
|
|
191
|
+
evidence=f"{tool_name} attempted before nexo_startup/session mapping.",
|
|
192
|
+
file_token="startup",
|
|
193
|
+
)
|
|
194
|
+
blocks.append(
|
|
195
|
+
{
|
|
196
|
+
"file": "",
|
|
197
|
+
"task_id": "",
|
|
198
|
+
"debt_id": debt.get("id"),
|
|
199
|
+
"debt_type": "strict_protocol_write_without_startup",
|
|
200
|
+
"reason_code": "missing_startup",
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
return {
|
|
204
|
+
"ok": True,
|
|
205
|
+
"session_id": sid,
|
|
206
|
+
"tool_name": tool_name,
|
|
207
|
+
"operation": op,
|
|
208
|
+
"strictness": strictness,
|
|
209
|
+
"blocks": blocks,
|
|
210
|
+
"status": "blocked",
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if not files:
|
|
214
|
+
task = _find_any_open_task(conn, sid)
|
|
215
|
+
if not task:
|
|
216
|
+
debt = _ensure_protocol_debt(
|
|
217
|
+
conn,
|
|
218
|
+
session_id=sid,
|
|
219
|
+
task_id="",
|
|
220
|
+
debt_type="strict_protocol_write_without_task",
|
|
221
|
+
severity="error",
|
|
222
|
+
evidence=f"{tool_name} attempted without a detectable file path and without an open protocol task.",
|
|
223
|
+
file_token="unknown-target",
|
|
224
|
+
)
|
|
225
|
+
blocks.append(
|
|
226
|
+
{
|
|
227
|
+
"file": "",
|
|
228
|
+
"task_id": "",
|
|
229
|
+
"debt_id": debt.get("id"),
|
|
230
|
+
"debt_type": "strict_protocol_write_without_task",
|
|
231
|
+
"reason_code": "missing_task",
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
return {
|
|
235
|
+
"ok": True,
|
|
236
|
+
"session_id": sid,
|
|
237
|
+
"tool_name": tool_name,
|
|
238
|
+
"operation": op,
|
|
239
|
+
"strictness": strictness,
|
|
240
|
+
"blocks": blocks,
|
|
241
|
+
"status": "blocked" if blocks else "clean",
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for filepath in files:
|
|
245
|
+
task = _find_open_task_for_file(conn, sid, filepath)
|
|
246
|
+
if not task:
|
|
247
|
+
debt = _ensure_protocol_debt(
|
|
248
|
+
conn,
|
|
249
|
+
session_id=sid,
|
|
250
|
+
task_id="",
|
|
251
|
+
debt_type="strict_protocol_write_without_task",
|
|
252
|
+
severity="error",
|
|
253
|
+
evidence=f"{tool_name} attempted on {filepath} without an open protocol task for that file.",
|
|
254
|
+
file_token=filepath,
|
|
255
|
+
)
|
|
256
|
+
blocks.append(
|
|
257
|
+
{
|
|
258
|
+
"file": filepath,
|
|
259
|
+
"task_id": "",
|
|
260
|
+
"debt_id": debt.get("id"),
|
|
261
|
+
"debt_type": "strict_protocol_write_without_task",
|
|
262
|
+
"reason_code": "missing_task",
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
guard_debt = _find_task_guard_blocking_debt(conn, task["task_id"])
|
|
268
|
+
if guard_debt:
|
|
269
|
+
debt = _ensure_protocol_debt(
|
|
270
|
+
conn,
|
|
271
|
+
session_id=sid,
|
|
272
|
+
task_id=task["task_id"],
|
|
273
|
+
debt_type="strict_protocol_write_without_guard_ack",
|
|
274
|
+
severity="error",
|
|
275
|
+
evidence=f"{tool_name} attempted on {filepath} before acknowledging guard debt for task {task['task_id']}.",
|
|
276
|
+
file_token=filepath,
|
|
277
|
+
)
|
|
278
|
+
blocks.append(
|
|
279
|
+
{
|
|
280
|
+
"file": filepath,
|
|
281
|
+
"task_id": task["task_id"],
|
|
282
|
+
"debt_id": debt.get("id"),
|
|
283
|
+
"debt_type": "strict_protocol_write_without_guard_ack",
|
|
284
|
+
"reason_code": "guard_unacknowledged",
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
"ok": True,
|
|
290
|
+
"session_id": sid,
|
|
291
|
+
"tool_name": tool_name,
|
|
292
|
+
"operation": op,
|
|
293
|
+
"strictness": strictness,
|
|
294
|
+
"blocks": blocks,
|
|
295
|
+
"status": "blocked" if blocks else "clean",
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
155
299
|
def process_tool_event(payload: dict) -> dict:
|
|
156
300
|
tool_name = str(payload.get("tool_name", "")).strip()
|
|
157
301
|
op = _operation_kind(tool_name)
|
|
@@ -289,6 +433,38 @@ def format_hook_message(result: dict) -> str:
|
|
|
289
433
|
return "\n".join(lines)
|
|
290
434
|
|
|
291
435
|
|
|
436
|
+
def format_pretool_block_message(result: dict) -> str:
|
|
437
|
+
blocks = result.get("blocks") or []
|
|
438
|
+
if not blocks:
|
|
439
|
+
return ""
|
|
440
|
+
strictness = str(result.get("strictness") or "strict")
|
|
441
|
+
header = (
|
|
442
|
+
"NEXO LEARNING MODE BLOCKED THIS EDIT:"
|
|
443
|
+
if strictness == "learning"
|
|
444
|
+
else "NEXO STRICT MODE BLOCKED THIS EDIT:"
|
|
445
|
+
)
|
|
446
|
+
lines = [header]
|
|
447
|
+
for item in blocks:
|
|
448
|
+
file_note = item["file"] or "(unknown target)"
|
|
449
|
+
if item.get("reason_code") == "missing_startup":
|
|
450
|
+
lines.append(
|
|
451
|
+
f"- Start the shared-brain session first: call `nexo_startup`, then `nexo_task_open`, before editing {file_note}."
|
|
452
|
+
)
|
|
453
|
+
elif item.get("reason_code") == "guard_unacknowledged":
|
|
454
|
+
lines.append(
|
|
455
|
+
f"- {file_note}: task {item['task_id']} still has blocking guard debt. Acknowledge it with `nexo_task_acknowledge_guard` before retrying."
|
|
456
|
+
)
|
|
457
|
+
elif strictness == "learning":
|
|
458
|
+
lines.append(
|
|
459
|
+
f"- {file_note}: open `nexo_task_open(task_type='edit', files=['{file_note}'])` first, then rerun the edit."
|
|
460
|
+
)
|
|
461
|
+
else:
|
|
462
|
+
lines.append(
|
|
463
|
+
f"- {file_note}: open `nexo_task_open(... files=['{file_note}'])` before editing."
|
|
464
|
+
)
|
|
465
|
+
return "\n".join(lines)
|
|
466
|
+
|
|
467
|
+
|
|
292
468
|
def main() -> int:
|
|
293
469
|
raw = sys.stdin.read()
|
|
294
470
|
if not raw.strip():
|
|
@@ -297,6 +473,12 @@ def main() -> int:
|
|
|
297
473
|
payload = json.loads(raw)
|
|
298
474
|
except Exception:
|
|
299
475
|
return 0
|
|
476
|
+
if os.environ.get("NEXO_HOOK_PHASE", "").strip().lower() == "pre":
|
|
477
|
+
result = process_pre_tool_event(payload)
|
|
478
|
+
message = format_pretool_block_message(result)
|
|
479
|
+
if message:
|
|
480
|
+
print(message, file=sys.stderr)
|
|
481
|
+
return 2 if result.get("status") == "blocked" else 0
|
|
300
482
|
result = process_tool_event(payload)
|
|
301
483
|
message = format_hook_message(result)
|
|
302
484
|
if message:
|
|
@@ -5,6 +5,6 @@ INPUT=$(cat || true)
|
|
|
5
5
|
[ -z "$INPUT" ] && exit 0
|
|
6
6
|
|
|
7
7
|
NEXO_CODE="${NEXO_CODE:-${HOME}/.nexo}"
|
|
8
|
-
python3 "$NEXO_CODE/hook_guardrails.py" <<< "$INPUT" 2>/dev/null || true
|
|
8
|
+
NEXO_HOOK_PHASE=post python3 "$NEXO_CODE/hook_guardrails.py" <<< "$INPUT" 2>/dev/null || true
|
|
9
9
|
|
|
10
10
|
exit 0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# NEXO PreToolUse hook — strict protocol blocking before writes/deletes
|
|
3
|
+
|
|
4
|
+
INPUT=$(cat || true)
|
|
5
|
+
[ -z "$INPUT" ] && exit 0
|
|
6
|
+
|
|
7
|
+
NEXO_CODE="${NEXO_CODE:-${HOME}/.nexo}"
|
|
8
|
+
NEXO_HOOK_PHASE=pre python3 "$NEXO_CODE/hook_guardrails.py" <<< "$INPUT"
|
|
9
|
+
exit $?
|
package/src/kg_populate.py
CHANGED
|
@@ -147,25 +147,27 @@ def backfill_decisions() -> int:
|
|
|
147
147
|
def backfill_somatic() -> int:
|
|
148
148
|
"""Read somatic_markers from cognitive.db → create file/area nodes with risk."""
|
|
149
149
|
cdb = _cognitive_db()
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
150
|
+
try:
|
|
151
|
+
rows = cdb.execute(
|
|
152
|
+
"SELECT target, target_type, risk_score, incident_count FROM somatic_markers"
|
|
153
|
+
).fetchall()
|
|
154
|
+
count = 0
|
|
155
|
+
for row in rows:
|
|
156
|
+
target_type = row["target_type"] or "file"
|
|
157
|
+
node_ref = f"{target_type}:{row['target']}"
|
|
158
|
+
kg.upsert_node(
|
|
159
|
+
node_type=target_type,
|
|
160
|
+
node_ref=node_ref,
|
|
161
|
+
label=os.path.basename(row["target"]) or row["target"],
|
|
162
|
+
properties={
|
|
163
|
+
"risk_score": row["risk_score"],
|
|
164
|
+
"incident_count": row["incident_count"],
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
count += 1
|
|
168
|
+
return count
|
|
169
|
+
finally:
|
|
170
|
+
cdb.close()
|
|
169
171
|
|
|
170
172
|
|
|
171
173
|
def run_full_backfill() -> dict:
|
package/src/maintenance.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Opportunistic maintenance — run overdue tasks on MCP startup."""
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
-
from datetime import datetime
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
5
|
from db import get_db
|
|
6
6
|
|
|
7
7
|
|
|
@@ -16,7 +16,7 @@ def check_and_run_overdue():
|
|
|
16
16
|
if last_run:
|
|
17
17
|
try:
|
|
18
18
|
last_dt = datetime.strptime(last_run, "%Y-%m-%dT%H:%M:%S")
|
|
19
|
-
hours_since = (datetime.now(
|
|
19
|
+
hours_since = (datetime.now(timezone.utc).replace(tzinfo=None) - last_dt).total_seconds() / 3600
|
|
20
20
|
if hours_since < interval:
|
|
21
21
|
continue
|
|
22
22
|
except (ValueError, TypeError):
|
|
@@ -28,7 +28,7 @@ def check_and_run_overdue():
|
|
|
28
28
|
conn.execute(
|
|
29
29
|
"UPDATE maintenance_schedule SET last_run_at = ?, last_duration_ms = ?, "
|
|
30
30
|
"run_count = run_count + 1 WHERE task_name = ?",
|
|
31
|
-
(datetime.now(
|
|
31
|
+
(datetime.now(timezone.utc).replace(tzinfo=None).strftime("%Y-%m-%dT%H:%M:%S"), duration_ms, task))
|
|
32
32
|
conn.commit()
|
|
33
33
|
ran.append({"task": task, "duration_ms": duration_ms})
|
|
34
34
|
except Exception as e:
|
|
@@ -30,15 +30,17 @@ MODELS = {
|
|
|
30
30
|
def verify():
|
|
31
31
|
"""Check current embedding dimensions in the database."""
|
|
32
32
|
conn = sqlite3.connect(DB_PATH)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
try:
|
|
34
|
+
for table in ["stm_memories", "ltm_memories"]:
|
|
35
|
+
count = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
|
|
36
|
+
if count == 0:
|
|
37
|
+
print(f" {table}: {count} rows (empty)")
|
|
38
|
+
continue
|
|
39
|
+
row = conn.execute(f"SELECT embedding FROM {table} LIMIT 1").fetchone()
|
|
40
|
+
vec = np.frombuffer(row[0], dtype=np.float32)
|
|
41
|
+
print(f" {table}: {count} rows, embedding dim = {len(vec)}")
|
|
42
|
+
finally:
|
|
43
|
+
conn.close()
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
def upgrade():
|
|
@@ -62,31 +64,31 @@ def upgrade():
|
|
|
62
64
|
model = TextEmbedding(model_name)
|
|
63
65
|
|
|
64
66
|
conn = sqlite3.connect(DB_PATH)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
67
|
+
try:
|
|
68
|
+
for table in ["stm_memories", "ltm_memories"]:
|
|
69
|
+
rows = conn.execute(f"SELECT id, content FROM {table}").fetchall()
|
|
70
|
+
if not rows:
|
|
71
|
+
print(f"\n{table}: empty, skipping")
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
print(f"\n{table}: re-embedding {len(rows)} memories...")
|
|
75
|
+
t0 = time.time()
|
|
76
|
+
|
|
77
|
+
# Batch embed for speed
|
|
78
|
+
contents = [r[1] for r in rows]
|
|
79
|
+
ids = [r[0] for r in rows]
|
|
80
|
+
|
|
81
|
+
embeddings = list(model.embed(contents))
|
|
82
|
+
|
|
83
|
+
for mem_id, emb in zip(ids, embeddings):
|
|
84
|
+
blob = np.array(emb, dtype=np.float32).tobytes()
|
|
85
|
+
conn.execute(f"UPDATE {table} SET embedding = ? WHERE id = ?", (blob, mem_id))
|
|
86
|
+
|
|
87
|
+
conn.commit()
|
|
88
|
+
elapsed = time.time() - t0
|
|
89
|
+
print(f" Done: {len(rows)} memories in {elapsed:.1f}s ({elapsed/len(rows)*1000:.0f}ms/memory)")
|
|
90
|
+
finally:
|
|
91
|
+
conn.close()
|
|
90
92
|
|
|
91
93
|
print("\nAfter upgrade:")
|
|
92
94
|
verify()
|
package/src/plugins/backup.py
CHANGED
|
@@ -21,10 +21,14 @@ def handle_backup_now() -> str:
|
|
|
21
21
|
# Use SQLite backup API for consistency
|
|
22
22
|
import sqlite3
|
|
23
23
|
src_conn = sqlite3.connect(DB_PATH)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
try:
|
|
25
|
+
dst_conn = sqlite3.connect(dest)
|
|
26
|
+
try:
|
|
27
|
+
src_conn.backup(dst_conn)
|
|
28
|
+
finally:
|
|
29
|
+
dst_conn.close()
|
|
30
|
+
finally:
|
|
31
|
+
src_conn.close()
|
|
28
32
|
|
|
29
33
|
size_kb = os.path.getsize(dest) / 1024
|
|
30
34
|
_cleanup_old()
|
|
@@ -63,17 +67,25 @@ def handle_backup_restore(filename: str) -> str:
|
|
|
63
67
|
safety = os.path.join(BACKUP_DIR, f"nexo-pre-restore-{time.strftime('%Y%m%d%H%M%S')}.db")
|
|
64
68
|
import sqlite3
|
|
65
69
|
src_conn = sqlite3.connect(DB_PATH)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
try:
|
|
71
|
+
dst_conn = sqlite3.connect(safety)
|
|
72
|
+
try:
|
|
73
|
+
src_conn.backup(dst_conn)
|
|
74
|
+
finally:
|
|
75
|
+
dst_conn.close()
|
|
76
|
+
finally:
|
|
77
|
+
src_conn.close()
|
|
70
78
|
|
|
71
79
|
# Restore
|
|
72
80
|
restore_conn = sqlite3.connect(src)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
try:
|
|
82
|
+
target_conn = sqlite3.connect(DB_PATH)
|
|
83
|
+
try:
|
|
84
|
+
restore_conn.backup(target_conn)
|
|
85
|
+
finally:
|
|
86
|
+
target_conn.close()
|
|
87
|
+
finally:
|
|
88
|
+
restore_conn.close()
|
|
77
89
|
|
|
78
90
|
# Invalidate shared connection so db.py reconnects to restored data
|
|
79
91
|
import db
|
package/src/plugins/protocol.py
CHANGED
|
@@ -23,6 +23,7 @@ from db import (
|
|
|
23
23
|
)
|
|
24
24
|
from plugins.cortex import evaluate_cortex_state
|
|
25
25
|
from plugins.guard import handle_guard_check
|
|
26
|
+
from protocol_settings import get_protocol_strictness
|
|
26
27
|
from tools_sessions import handle_heartbeat
|
|
27
28
|
|
|
28
29
|
|
|
@@ -450,6 +451,18 @@ def handle_task_open(
|
|
|
450
451
|
|
|
451
452
|
clean_type = task_type if task_type in {"answer", "analyze", "edit", "execute", "delegate"} else "answer"
|
|
452
453
|
files_list = _parse_list(files)
|
|
454
|
+
protocol_strictness = get_protocol_strictness()
|
|
455
|
+
if protocol_strictness in {"strict", "learning"} and clean_type == "edit" and not files_list:
|
|
456
|
+
note = (
|
|
457
|
+
"Strict protocol mode requires explicit `files` for edit tasks."
|
|
458
|
+
if protocol_strictness == "strict"
|
|
459
|
+
else "Learning mode requires explicit `files` on edit tasks so NEXO can match the write against the open protocol task."
|
|
460
|
+
)
|
|
461
|
+
return json.dumps(
|
|
462
|
+
{"ok": False, "error": note, "protocol_strictness": protocol_strictness},
|
|
463
|
+
ensure_ascii=False,
|
|
464
|
+
indent=2,
|
|
465
|
+
)
|
|
453
466
|
state = {
|
|
454
467
|
"goal": clean_goal,
|
|
455
468
|
"task_type": clean_type,
|
|
@@ -569,6 +582,7 @@ def handle_task_open(
|
|
|
569
582
|
"session_id": sid,
|
|
570
583
|
"goal": clean_goal,
|
|
571
584
|
"task_type": clean_type,
|
|
585
|
+
"protocol_strictness": protocol_strictness,
|
|
572
586
|
"mode": cortex["mode"],
|
|
573
587
|
"check_id": cortex["check_id"],
|
|
574
588
|
"blocked_reason": cortex.get("blocked_reason"),
|
|
@@ -596,6 +610,7 @@ def handle_task_open(
|
|
|
596
610
|
"must_change_log": must_change_log,
|
|
597
611
|
"must_learning_if_corrected": must_learning_if_corrected,
|
|
598
612
|
"must_write_diary_on_close": must_write_diary_on_close,
|
|
613
|
+
"protocol_strictness": protocol_strictness,
|
|
599
614
|
},
|
|
600
615
|
"session_touch": heartbeat_result.splitlines()[0] if heartbeat_result else "",
|
|
601
616
|
"open_debts": debts_created,
|