nexo-brain 3.1.2 → 3.1.4
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/package.json +1 -1
- package/src/dashboard/app.py +131 -106
- package/src/db/__init__.py +3 -2
- package/src/db/_core.py +20 -1
- package/src/db/_reminders.py +499 -129
- package/src/db/_schema.py +30 -0
- package/src/scripts/deep-sleep/apply_findings.py +37 -34
- package/src/scripts/nexo-daily-self-audit.py +26 -42
- package/src/scripts/nexo-followup-hygiene.py +29 -2
- package/src/server.py +121 -28
- package/src/tools_learnings.py +12 -2
- package/src/tools_reminders.py +8 -5
- package/src/tools_reminders_crud.py +200 -18
package/src/db/_schema.py
CHANGED
|
@@ -674,6 +674,35 @@ def _m28_automation_runs(conn):
|
|
|
674
674
|
_migrate_add_index(conn, "idx_automation_runs_status", "automation_runs", "status")
|
|
675
675
|
|
|
676
676
|
|
|
677
|
+
def _m29_item_history_and_soft_delete(conn):
|
|
678
|
+
"""Persist reminder/followup history and read-before-mutate tokens."""
|
|
679
|
+
conn.execute("""
|
|
680
|
+
CREATE TABLE IF NOT EXISTS item_history (
|
|
681
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
682
|
+
item_type TEXT NOT NULL,
|
|
683
|
+
item_id TEXT NOT NULL,
|
|
684
|
+
event_type TEXT NOT NULL,
|
|
685
|
+
note TEXT DEFAULT '',
|
|
686
|
+
actor TEXT DEFAULT '',
|
|
687
|
+
metadata TEXT DEFAULT '{}',
|
|
688
|
+
created_at REAL NOT NULL
|
|
689
|
+
)
|
|
690
|
+
""")
|
|
691
|
+
conn.execute("""
|
|
692
|
+
CREATE TABLE IF NOT EXISTS item_read_tokens (
|
|
693
|
+
token TEXT PRIMARY KEY,
|
|
694
|
+
item_type TEXT NOT NULL,
|
|
695
|
+
item_id TEXT NOT NULL,
|
|
696
|
+
history_seq INTEGER DEFAULT 0,
|
|
697
|
+
issued_at REAL NOT NULL,
|
|
698
|
+
expires_at REAL NOT NULL
|
|
699
|
+
)
|
|
700
|
+
""")
|
|
701
|
+
_migrate_add_index(conn, "idx_item_history_lookup", "item_history", "item_type, item_id, created_at")
|
|
702
|
+
_migrate_add_index(conn, "idx_item_history_item", "item_history", "item_id")
|
|
703
|
+
_migrate_add_index(conn, "idx_item_read_tokens_lookup", "item_read_tokens", "item_type, item_id, expires_at")
|
|
704
|
+
|
|
705
|
+
|
|
677
706
|
MIGRATIONS = [
|
|
678
707
|
(1, "learnings_columns", _m1_learnings_columns),
|
|
679
708
|
(2, "followups_reasoning", _m2_followups_reasoning),
|
|
@@ -703,6 +732,7 @@ MIGRATIONS = [
|
|
|
703
732
|
(26, "protocol_answer_confidence", _m26_protocol_answer_confidence),
|
|
704
733
|
(27, "state_watchers", _m27_state_watchers),
|
|
705
734
|
(28, "automation_runs", _m28_automation_runs),
|
|
735
|
+
(29, "item_history_and_soft_delete", _m29_item_history_and_soft_delete),
|
|
706
736
|
]
|
|
707
737
|
|
|
708
738
|
|
|
@@ -30,6 +30,8 @@ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent
|
|
|
30
30
|
if str(NEXO_CODE) not in sys.path:
|
|
31
31
|
sys.path.insert(0, str(NEXO_CODE))
|
|
32
32
|
|
|
33
|
+
import db as nexo_db
|
|
34
|
+
|
|
33
35
|
DEEP_SLEEP_DIR = NEXO_HOME / "operations" / "deep-sleep"
|
|
34
36
|
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
35
37
|
COGNITIVE_DB = NEXO_HOME / "data" / "cognitive.db"
|
|
@@ -294,25 +296,31 @@ def _touch_existing_followup(existing: dict, *, description: str, date: str = ""
|
|
|
294
296
|
preferred_date = _prefer_due_date(existing.get("date", ""), date)
|
|
295
297
|
if preferred_date and preferred_date != str(existing.get("date", "") or "") and "date" in cols:
|
|
296
298
|
updates["date"] = preferred_date
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if "updated_at" in cols:
|
|
300
|
-
updates["updated_at"] = datetime.now().timestamp()
|
|
301
|
-
|
|
299
|
+
note = reasoning_note or "Deep Sleep matched this followup semantically."
|
|
300
|
+
changed = False
|
|
302
301
|
if updates:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
302
|
+
result = nexo_db.update_followup(
|
|
303
|
+
str(existing["id"]),
|
|
304
|
+
history_actor="deep-sleep",
|
|
305
|
+
history_event="updated",
|
|
306
|
+
history_note=note,
|
|
307
|
+
**updates,
|
|
308
|
+
)
|
|
309
|
+
if result.get("error"):
|
|
310
|
+
return {"success": False, "error": result["error"]}
|
|
311
|
+
changed = True
|
|
312
|
+
elif note:
|
|
313
|
+
note_result = nexo_db.add_followup_note(str(existing["id"]), note, actor="deep-sleep")
|
|
314
|
+
if note_result.get("error"):
|
|
315
|
+
return {"success": False, "error": note_result["error"]}
|
|
316
|
+
changed = True
|
|
309
317
|
|
|
310
318
|
return {
|
|
311
319
|
"success": True,
|
|
312
320
|
"id": existing["id"],
|
|
313
321
|
"outcome": "matched_existing_followup",
|
|
314
322
|
"similarity": existing.get("_similarity", 1.0),
|
|
315
|
-
"updated_existing":
|
|
323
|
+
"updated_existing": changed,
|
|
316
324
|
}
|
|
317
325
|
|
|
318
326
|
|
|
@@ -509,32 +517,27 @@ def create_followup(description: str, date: str = "", reasoning_note: str = "")
|
|
|
509
517
|
reasoning_note=reasoning_note or "Deep Sleep matched this followup semantically.",
|
|
510
518
|
)
|
|
511
519
|
|
|
512
|
-
now = datetime.now().timestamp()
|
|
513
520
|
# Generate a deterministic ID
|
|
514
521
|
fid = "NF-DS-" + hashlib.md5(description.encode()).hexdigest()[:8].upper()
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
if "reasoning" in columns:
|
|
525
|
-
payload["reasoning"] = reasoning_note or "Deep Sleep v2 overnight analysis"
|
|
526
|
-
if "verification" in columns:
|
|
527
|
-
payload["verification"] = ""
|
|
528
|
-
insert_columns = [column for column in payload if column in columns]
|
|
529
|
-
values = [payload[column] for column in insert_columns]
|
|
522
|
+
existing = nexo_db.get_followup(fid)
|
|
523
|
+
if existing:
|
|
524
|
+
return _touch_existing_followup(
|
|
525
|
+
existing,
|
|
526
|
+
description=description,
|
|
527
|
+
date=date,
|
|
528
|
+
reasoning_note=reasoning_note or "Deep Sleep revisited this deterministic followup.",
|
|
529
|
+
)
|
|
530
530
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
531
|
+
followup_result = nexo_db.create_followup(
|
|
532
|
+
id=fid,
|
|
533
|
+
description=description,
|
|
534
|
+
date=date or None,
|
|
535
|
+
verification="",
|
|
536
|
+
reasoning=reasoning_note or "Deep Sleep v2 overnight analysis",
|
|
537
|
+
recurrence=None,
|
|
535
538
|
)
|
|
536
|
-
|
|
537
|
-
|
|
539
|
+
if followup_result.get("error"):
|
|
540
|
+
return {"success": False, "error": followup_result["error"]}
|
|
538
541
|
return {"success": True, "id": fid, "outcome": "new_followup"}
|
|
539
542
|
except Exception as e:
|
|
540
543
|
return {"success": False, "error": str(e)}
|
|
@@ -37,6 +37,7 @@ if str(NEXO_CODE) not in sys.path:
|
|
|
37
37
|
sys.path.insert(0, str(NEXO_CODE))
|
|
38
38
|
|
|
39
39
|
from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
|
|
40
|
+
import db as nexo_db
|
|
40
41
|
from public_evolution_queue import queue_public_port_candidate
|
|
41
42
|
|
|
42
43
|
LOG_DIR = NEXO_HOME / "logs"
|
|
@@ -251,42 +252,36 @@ def _ensure_followup(conn: sqlite3.Connection, *, prefix: str, description: str,
|
|
|
251
252
|
"description": description,
|
|
252
253
|
"verification": verification,
|
|
253
254
|
"reasoning": reasoning,
|
|
254
|
-
"updated_at": now_epoch,
|
|
255
255
|
}
|
|
256
256
|
if "priority" in columns:
|
|
257
257
|
update_fields["priority"] = priority
|
|
258
258
|
closed_status = str(existing_id_row["status"] or "").upper()
|
|
259
259
|
if closed_status.startswith("COMPLETED") or closed_status in {"DELETED", "ARCHIVED", "BLOCKED", "WAITING"}:
|
|
260
260
|
update_fields["status"] = "PENDING"
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
261
|
+
conn.commit()
|
|
262
|
+
result = nexo_db.update_followup(
|
|
263
|
+
followup_id,
|
|
264
|
+
history_actor="self-audit",
|
|
265
|
+
history_event="updated",
|
|
266
|
+
history_note="Daily self-audit refreshed canonical followup coverage.",
|
|
267
|
+
**update_fields,
|
|
268
|
+
)
|
|
269
|
+
if result.get("error"):
|
|
270
|
+
return ""
|
|
268
271
|
return followup_id
|
|
269
272
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
"updated_at": now_epoch,
|
|
280
|
-
}
|
|
281
|
-
if "priority" in columns:
|
|
282
|
-
values["priority"] = priority
|
|
283
|
-
|
|
284
|
-
ordered_columns = [name for name in values.keys() if name in columns]
|
|
285
|
-
placeholders = ", ".join("?" for _ in ordered_columns)
|
|
286
|
-
conn.execute(
|
|
287
|
-
f"INSERT INTO followups ({', '.join(ordered_columns)}) VALUES ({placeholders})",
|
|
288
|
-
[values[name] for name in ordered_columns],
|
|
273
|
+
conn.commit()
|
|
274
|
+
result = nexo_db.create_followup(
|
|
275
|
+
id=followup_id,
|
|
276
|
+
description=description,
|
|
277
|
+
date=None,
|
|
278
|
+
verification=verification,
|
|
279
|
+
reasoning=reasoning,
|
|
280
|
+
recurrence=None,
|
|
281
|
+
priority=priority,
|
|
289
282
|
)
|
|
283
|
+
if result.get("error"):
|
|
284
|
+
return ""
|
|
290
285
|
return followup_id
|
|
291
286
|
|
|
292
287
|
|
|
@@ -319,7 +314,6 @@ def _append_note(existing: str, note: str) -> str:
|
|
|
319
314
|
def _complete_matching_followup(conn: sqlite3.Connection, description: str, note: str) -> int:
|
|
320
315
|
if not _table_exists(conn, "followups"):
|
|
321
316
|
return 0
|
|
322
|
-
columns = _table_columns(conn, "followups")
|
|
323
317
|
rows = conn.execute(
|
|
324
318
|
"""SELECT id, verification, reasoning
|
|
325
319
|
FROM followups
|
|
@@ -329,21 +323,11 @@ def _complete_matching_followup(conn: sqlite3.Connection, description: str, note
|
|
|
329
323
|
(description,),
|
|
330
324
|
).fetchall()
|
|
331
325
|
completed = 0
|
|
332
|
-
|
|
326
|
+
conn.commit()
|
|
333
327
|
for row in rows:
|
|
334
|
-
|
|
335
|
-
if "
|
|
336
|
-
|
|
337
|
-
if "verification" in columns:
|
|
338
|
-
updates["verification"] = _append_note(row["verification"], note)
|
|
339
|
-
if "reasoning" in columns:
|
|
340
|
-
updates["reasoning"] = _append_note(row["reasoning"], note)
|
|
341
|
-
assignments = ", ".join(f"{column} = ?" for column in updates)
|
|
342
|
-
conn.execute(
|
|
343
|
-
f"UPDATE followups SET {assignments} WHERE id = ?",
|
|
344
|
-
[updates[column] for column in updates] + [row["id"]],
|
|
345
|
-
)
|
|
346
|
-
completed += 1
|
|
328
|
+
result = nexo_db.complete_followup(str(row["id"]), note)
|
|
329
|
+
if not result.get("error"):
|
|
330
|
+
completed += 1
|
|
347
331
|
return completed
|
|
348
332
|
|
|
349
333
|
|
|
@@ -18,6 +18,13 @@ from datetime import datetime, date, timedelta
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
20
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
21
|
+
_script_dir = Path(__file__).resolve().parent
|
|
22
|
+
_repo_src = _script_dir.parent
|
|
23
|
+
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
|
|
24
|
+
if str(NEXO_CODE) not in sys.path:
|
|
25
|
+
sys.path.insert(0, str(NEXO_CODE))
|
|
26
|
+
|
|
27
|
+
import db as nexo_db
|
|
21
28
|
|
|
22
29
|
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
23
30
|
COORD_DIR = NEXO_HOME / "coordination"
|
|
@@ -50,11 +57,31 @@ def main():
|
|
|
50
57
|
dirty_r = conn.execute("SELECT COUNT(*) FROM reminders WHERE status LIKE 'COMPLETED %'").fetchone()[0]
|
|
51
58
|
|
|
52
59
|
if dirty_f > 0:
|
|
53
|
-
conn.execute(
|
|
60
|
+
dirty_followups = conn.execute(
|
|
61
|
+
"SELECT id, status FROM followups WHERE status LIKE 'COMPLETED %'"
|
|
62
|
+
).fetchall()
|
|
63
|
+
for row in dirty_followups:
|
|
64
|
+
nexo_db.update_followup(
|
|
65
|
+
str(row["id"]),
|
|
66
|
+
status="COMPLETED",
|
|
67
|
+
history_actor="followup-hygiene",
|
|
68
|
+
history_event="normalized",
|
|
69
|
+
history_note=f"Weekly hygiene normalized dirty status from {row['status']} to COMPLETED.",
|
|
70
|
+
)
|
|
54
71
|
log(f"Normalized {dirty_f} dirty followup statuses")
|
|
55
72
|
|
|
56
73
|
if dirty_r > 0:
|
|
57
|
-
conn.execute(
|
|
74
|
+
dirty_reminders = conn.execute(
|
|
75
|
+
"SELECT id, status FROM reminders WHERE status LIKE 'COMPLETED %'"
|
|
76
|
+
).fetchall()
|
|
77
|
+
for row in dirty_reminders:
|
|
78
|
+
nexo_db.update_reminder(
|
|
79
|
+
str(row["id"]),
|
|
80
|
+
status="COMPLETED",
|
|
81
|
+
history_actor="followup-hygiene",
|
|
82
|
+
history_event="normalized",
|
|
83
|
+
history_note=f"Weekly hygiene normalized dirty status from {row['status']} to COMPLETED.",
|
|
84
|
+
)
|
|
58
85
|
log(f"Normalized {dirty_r} dirty reminder statuses")
|
|
59
86
|
|
|
60
87
|
# 2. Flag stale followups (PENDING >14 days, no updates)
|
package/src/server.py
CHANGED
|
@@ -24,10 +24,10 @@ from tools_coordination import (
|
|
|
24
24
|
from tools_reminders import handle_reminders
|
|
25
25
|
from tools_menu import handle_menu
|
|
26
26
|
from tools_reminders_crud import (
|
|
27
|
-
handle_reminder_create, handle_reminder_update,
|
|
28
|
-
handle_reminder_complete, handle_reminder_delete,
|
|
29
|
-
handle_followup_create, handle_followup_update,
|
|
30
|
-
handle_followup_complete, handle_followup_delete,
|
|
27
|
+
handle_reminder_create, handle_reminder_get, handle_reminder_update,
|
|
28
|
+
handle_reminder_complete, handle_reminder_note, handle_reminder_restore, handle_reminder_delete,
|
|
29
|
+
handle_followup_create, handle_followup_get, handle_followup_update,
|
|
30
|
+
handle_followup_complete, handle_followup_note, handle_followup_restore, handle_followup_delete,
|
|
31
31
|
)
|
|
32
32
|
from tools_learnings import (
|
|
33
33
|
handle_learning_add, handle_learning_search,
|
|
@@ -191,6 +191,8 @@ mcp = FastMCP(
|
|
|
191
191
|
"React: DIARY REMINDER→write diary, VIBE:NEGATIVE→ultra-concise, AUTO-PRIME→read learnings\n"
|
|
192
192
|
"- **Followups:** NEXO tasks, execute silently. 'done'/'all set'→`nexo_followup_complete` NOW. "
|
|
193
193
|
"Reminders=user's, alert when due\n"
|
|
194
|
+
"- **Reminder/followup history:** before update/delete/restore/note, call the corresponding "
|
|
195
|
+
"`nexo_reminder_get` / `nexo_followup_get` first and use its `READ_TOKEN`.\n"
|
|
194
196
|
"- **Observe:** correction→learning. 'tomorrow'→followup. person→entity. open topic→followup 3d\n"
|
|
195
197
|
"- **Trust events:** When user expresses satisfaction/thanks (any language)→`nexo_cognitive_trust(event='explicit_thanks')`. "
|
|
196
198
|
"When user corrects you→`nexo_cognitive_trust(event='correction')`. "
|
|
@@ -488,7 +490,7 @@ def nexo_reminders(filter: str = "due") -> str:
|
|
|
488
490
|
"""Check reminders and followups.
|
|
489
491
|
|
|
490
492
|
Args:
|
|
491
|
-
filter: 'due'
|
|
493
|
+
filter: 'due', 'all', 'followups', 'completed', 'deleted', 'history', or 'any'
|
|
492
494
|
"""
|
|
493
495
|
return handle_reminders(filter)
|
|
494
496
|
|
|
@@ -503,7 +505,7 @@ def nexo_menu() -> str:
|
|
|
503
505
|
return handle_menu()
|
|
504
506
|
|
|
505
507
|
|
|
506
|
-
# ── Reminders CRUD (
|
|
508
|
+
# ── Reminders CRUD (7 tools) ──────────────────────────────────────
|
|
507
509
|
|
|
508
510
|
@mcp.tool
|
|
509
511
|
def nexo_reminder_create(id: str, description: str, date: str = "", category: str = "general") -> str:
|
|
@@ -519,17 +521,36 @@ def nexo_reminder_create(id: str, description: str, date: str = "", category: st
|
|
|
519
521
|
|
|
520
522
|
|
|
521
523
|
@mcp.tool
|
|
522
|
-
def
|
|
524
|
+
def nexo_reminder_get(id: str) -> str:
|
|
525
|
+
"""Read a reminder with its history and usage rules.
|
|
526
|
+
|
|
527
|
+
IMPORTANT: before update/delete/restore/note, call this tool first and use the returned READ_TOKEN.
|
|
528
|
+
"""
|
|
529
|
+
return handle_reminder_get(id)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@mcp.tool
|
|
533
|
+
def nexo_reminder_update(
|
|
534
|
+
id: str,
|
|
535
|
+
description: str = "",
|
|
536
|
+
date: str = "",
|
|
537
|
+
status: str = "",
|
|
538
|
+
category: str = "",
|
|
539
|
+
read_token: str = "",
|
|
540
|
+
) -> str:
|
|
523
541
|
"""Update fields of an existing reminder. Only non-empty fields are changed.
|
|
524
542
|
|
|
543
|
+
IMPORTANT: call `nexo_reminder_get` first and pass its READ_TOKEN.
|
|
544
|
+
|
|
525
545
|
Args:
|
|
526
546
|
id: Reminder ID (e.g., R87).
|
|
527
547
|
description: New description (optional).
|
|
528
548
|
date: New date YYYY-MM-DD (optional).
|
|
529
549
|
status: New status (optional).
|
|
530
550
|
category: New category (optional).
|
|
551
|
+
read_token: Token returned by `nexo_reminder_get`.
|
|
531
552
|
"""
|
|
532
|
-
return handle_reminder_update(id, description, date, status, category)
|
|
553
|
+
return handle_reminder_update(id, description, date, status, category, read_token)
|
|
533
554
|
|
|
534
555
|
|
|
535
556
|
@mcp.tool
|
|
@@ -543,16 +564,47 @@ def nexo_reminder_complete(id: str) -> str:
|
|
|
543
564
|
|
|
544
565
|
|
|
545
566
|
@mcp.tool
|
|
546
|
-
def
|
|
547
|
-
"""
|
|
567
|
+
def nexo_reminder_note(id: str, note: str, read_token: str = "", actor: str = "nexo") -> str:
|
|
568
|
+
"""Append a note to reminder history.
|
|
569
|
+
|
|
570
|
+
IMPORTANT: call `nexo_reminder_get` first and pass its READ_TOKEN.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
id: Reminder ID (e.g., R87).
|
|
574
|
+
note: Operational note to append to history.
|
|
575
|
+
read_token: Token returned by `nexo_reminder_get`.
|
|
576
|
+
actor: Actor label for the history note.
|
|
577
|
+
"""
|
|
578
|
+
return handle_reminder_note(id, note, read_token, actor)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@mcp.tool
|
|
582
|
+
def nexo_reminder_restore(id: str, read_token: str = "") -> str:
|
|
583
|
+
"""Restore a soft-deleted reminder back to PENDING.
|
|
584
|
+
|
|
585
|
+
IMPORTANT: call `nexo_reminder_get` first and pass its READ_TOKEN.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
id: Reminder ID (e.g., R87).
|
|
589
|
+
read_token: Token returned by `nexo_reminder_get`.
|
|
590
|
+
"""
|
|
591
|
+
return handle_reminder_restore(id, read_token)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@mcp.tool
|
|
595
|
+
def nexo_reminder_delete(id: str, read_token: str = "") -> str:
|
|
596
|
+
"""Soft-delete a reminder.
|
|
597
|
+
|
|
598
|
+
IMPORTANT: call `nexo_reminder_get` first and pass its READ_TOKEN.
|
|
548
599
|
|
|
549
600
|
Args:
|
|
550
601
|
id: Reminder ID (e.g., R87).
|
|
602
|
+
read_token: Token returned by `nexo_reminder_get`.
|
|
551
603
|
"""
|
|
552
|
-
return handle_reminder_delete(id)
|
|
604
|
+
return handle_reminder_delete(id, read_token)
|
|
553
605
|
|
|
554
606
|
|
|
555
|
-
# ── Followups CRUD (
|
|
607
|
+
# ── Followups CRUD (7 tools) ──────────────────────────────────────
|
|
556
608
|
|
|
557
609
|
@mcp.tool
|
|
558
610
|
def nexo_followup_create(id: str, description: str, date: str = "", verification: str = "", reasoning: str = "", recurrence: str = "", priority: str = "medium") -> str:
|
|
@@ -568,18 +620,32 @@ def nexo_followup_create(id: str, description: str, date: str = "", verification
|
|
|
568
620
|
When completed, a new followup is auto-created with the next date. The completed one is archived with date suffix.
|
|
569
621
|
priority: critical, high, medium, low (default: medium).
|
|
570
622
|
"""
|
|
571
|
-
|
|
572
|
-
if priority in ('critical', 'high', 'low') and 'created' in result:
|
|
573
|
-
from db import get_db
|
|
574
|
-
get_db().execute("UPDATE followups SET priority = ? WHERE id = ?", (priority, id))
|
|
575
|
-
get_db().commit()
|
|
576
|
-
return result
|
|
623
|
+
return handle_followup_create(id, description, date, verification, reasoning, recurrence, priority)
|
|
577
624
|
|
|
578
625
|
|
|
579
626
|
@mcp.tool
|
|
580
|
-
def
|
|
627
|
+
def nexo_followup_get(id: str) -> str:
|
|
628
|
+
"""Read a followup with its history and usage rules.
|
|
629
|
+
|
|
630
|
+
IMPORTANT: before update/delete/restore/note, call this tool first and use the returned READ_TOKEN.
|
|
631
|
+
"""
|
|
632
|
+
return handle_followup_get(id)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@mcp.tool
|
|
636
|
+
def nexo_followup_update(
|
|
637
|
+
id: str,
|
|
638
|
+
description: str = "",
|
|
639
|
+
date: str = "",
|
|
640
|
+
verification: str = "",
|
|
641
|
+
status: str = "",
|
|
642
|
+
priority: str = "",
|
|
643
|
+
read_token: str = "",
|
|
644
|
+
) -> str:
|
|
581
645
|
"""Update fields of an existing followup. Only non-empty fields are changed.
|
|
582
646
|
|
|
647
|
+
IMPORTANT: call `nexo_followup_get` first and pass its READ_TOKEN.
|
|
648
|
+
|
|
583
649
|
Args:
|
|
584
650
|
id: Followup ID (e.g., NF45).
|
|
585
651
|
description: New description (optional).
|
|
@@ -587,13 +653,9 @@ def nexo_followup_update(id: str, description: str = "", date: str = "", verific
|
|
|
587
653
|
verification: New verification text (optional).
|
|
588
654
|
status: New status (optional).
|
|
589
655
|
priority: critical, high, medium, low (optional).
|
|
656
|
+
read_token: Token returned by `nexo_followup_get`.
|
|
590
657
|
"""
|
|
591
|
-
|
|
592
|
-
if priority in ('critical', 'high', 'medium', 'low'):
|
|
593
|
-
from db import get_db
|
|
594
|
-
get_db().execute("UPDATE followups SET priority = ? WHERE id = ?", (priority, id))
|
|
595
|
-
get_db().commit()
|
|
596
|
-
return result
|
|
658
|
+
return handle_followup_update(id, description, date, verification, status, priority, read_token)
|
|
597
659
|
|
|
598
660
|
|
|
599
661
|
@mcp.tool
|
|
@@ -608,13 +670,44 @@ def nexo_followup_complete(id: str, result: str = "") -> str:
|
|
|
608
670
|
|
|
609
671
|
|
|
610
672
|
@mcp.tool
|
|
611
|
-
def
|
|
612
|
-
"""
|
|
673
|
+
def nexo_followup_note(id: str, note: str, read_token: str = "", actor: str = "nexo") -> str:
|
|
674
|
+
"""Append a note to followup history.
|
|
675
|
+
|
|
676
|
+
IMPORTANT: call `nexo_followup_get` first and pass its READ_TOKEN.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
id: Followup ID (e.g., NF45).
|
|
680
|
+
note: Operational note to append to history.
|
|
681
|
+
read_token: Token returned by `nexo_followup_get`.
|
|
682
|
+
actor: Actor label for the history note.
|
|
683
|
+
"""
|
|
684
|
+
return handle_followup_note(id, note, read_token, actor)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
@mcp.tool
|
|
688
|
+
def nexo_followup_restore(id: str, read_token: str = "") -> str:
|
|
689
|
+
"""Restore a soft-deleted followup back to PENDING.
|
|
690
|
+
|
|
691
|
+
IMPORTANT: call `nexo_followup_get` first and pass its READ_TOKEN.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
id: Followup ID (e.g., NF45).
|
|
695
|
+
read_token: Token returned by `nexo_followup_get`.
|
|
696
|
+
"""
|
|
697
|
+
return handle_followup_restore(id, read_token)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
@mcp.tool
|
|
701
|
+
def nexo_followup_delete(id: str, read_token: str = "") -> str:
|
|
702
|
+
"""Soft-delete a followup.
|
|
703
|
+
|
|
704
|
+
IMPORTANT: call `nexo_followup_get` first and pass its READ_TOKEN.
|
|
613
705
|
|
|
614
706
|
Args:
|
|
615
707
|
id: Followup ID (e.g., NF45).
|
|
708
|
+
read_token: Token returned by `nexo_followup_get`.
|
|
616
709
|
"""
|
|
617
|
-
return handle_followup_delete(id)
|
|
710
|
+
return handle_followup_delete(id, read_token)
|
|
618
711
|
|
|
619
712
|
|
|
620
713
|
# ── Learnings CRUD (5 tools) ──────────────────────────────────────
|
package/src/tools_learnings.py
CHANGED
|
@@ -307,16 +307,26 @@ def handle_learning_add(category: str, title: str, content: str, reasoning: str
|
|
|
307
307
|
return f"ERROR: {result['error']}"
|
|
308
308
|
if prevention or applies_to or review_days > 0 or priority != 'medium':
|
|
309
309
|
initial_weight = {'critical': 0.9, 'high': 0.7, 'medium': 0.5, 'low': 0.3}[priority]
|
|
310
|
+
updated_at = now_epoch()
|
|
311
|
+
review_due_at = now_epoch() + (max(1, int(review_days)) * 86400)
|
|
310
312
|
conn = get_db()
|
|
311
313
|
conn.execute(
|
|
312
314
|
"UPDATE learnings SET prevention = ?, applies_to = ?, status = COALESCE(status, 'active'), "
|
|
313
315
|
"review_due_at = ?, updated_at = ?, priority = ?, weight = ? WHERE id = ?",
|
|
314
|
-
(prevention, applies_to,
|
|
316
|
+
(prevention, applies_to, review_due_at, updated_at,
|
|
315
317
|
priority, initial_weight, result["id"])
|
|
316
318
|
)
|
|
317
319
|
conn.commit()
|
|
318
|
-
result = conn.execute("SELECT * FROM learnings WHERE id = ?", (result["id"],)).fetchone()
|
|
319
320
|
result = dict(result)
|
|
321
|
+
result.update({
|
|
322
|
+
"prevention": prevention,
|
|
323
|
+
"applies_to": applies_to,
|
|
324
|
+
"status": result.get("status") or "active",
|
|
325
|
+
"review_due_at": review_due_at,
|
|
326
|
+
"updated_at": updated_at,
|
|
327
|
+
"priority": priority,
|
|
328
|
+
"weight": initial_weight,
|
|
329
|
+
})
|
|
320
330
|
|
|
321
331
|
# Cognitive ingest — embed learning for semantic search
|
|
322
332
|
new_id = result["id"]
|
package/src/tools_reminders.py
CHANGED
|
@@ -19,16 +19,16 @@ def handle_reminders(filter_type: str = "due") -> str:
|
|
|
19
19
|
"""Read reminders and followups from SQLite, return relevant ones.
|
|
20
20
|
|
|
21
21
|
Args:
|
|
22
|
-
filter_type: 'due'
|
|
22
|
+
filter_type: 'due', 'all', 'followups', 'completed', 'deleted', 'history', 'any'
|
|
23
23
|
"""
|
|
24
24
|
parts = []
|
|
25
25
|
|
|
26
|
-
if filter_type in ("due", "all"):
|
|
26
|
+
if filter_type in ("due", "all", "completed", "deleted", "history", "any"):
|
|
27
27
|
r = _format_reminders(filter_type)
|
|
28
28
|
if r:
|
|
29
29
|
parts.append(r)
|
|
30
30
|
|
|
31
|
-
if filter_type in ("due", "all", "followups"):
|
|
31
|
+
if filter_type in ("due", "all", "followups", "completed", "deleted", "history", "any"):
|
|
32
32
|
f = _format_followups(filter_type)
|
|
33
33
|
if f:
|
|
34
34
|
parts.append(f)
|
|
@@ -52,7 +52,8 @@ def _format_reminders(filter_type: str) -> str:
|
|
|
52
52
|
desc = desc.replace("**", "")
|
|
53
53
|
due_marker = " [DUE]" if _is_due(fecha) else ""
|
|
54
54
|
fecha_display = f"({fecha})" if fecha else "(—)"
|
|
55
|
-
|
|
55
|
+
status_tag = f" [{status}]" if status and status != "PENDING" else ""
|
|
56
|
+
lines.append(f" {rid} {fecha_display}{due_marker}{status_tag} — {desc[:120]}")
|
|
56
57
|
if "RECURRENTE" in status.upper():
|
|
57
58
|
lines.append(f" Status: {status}")
|
|
58
59
|
|
|
@@ -81,6 +82,8 @@ def _format_followups(filter_type: str) -> str:
|
|
|
81
82
|
pri = r.get("priority") or "medium"
|
|
82
83
|
pri_icon = {"critical": "🔴", "high": "🟠", "medium": "", "low": "⚪"}.get(pri, "")
|
|
83
84
|
pri_tag = f" {pri_icon}" if pri_icon else ""
|
|
84
|
-
|
|
85
|
+
status = r.get("status") or ""
|
|
86
|
+
status_tag = f" [{status}]" if status and status != "PENDING" else ""
|
|
87
|
+
lines.append(f" {nfid} {fecha_display}{due_marker}{pri_tag}{rec_tag}{status_tag} — {desc[:120]}")
|
|
85
88
|
|
|
86
89
|
return "\n".join(lines)
|