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/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
- if "reasoning" in cols and reasoning_note:
298
- updates["reasoning"] = _append_note(existing.get("reasoning", ""), reasoning_note)
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
- conn = sqlite3.connect(str(NEXO_DB))
304
- set_clause = ", ".join(f"{column} = ?" for column in updates)
305
- params = list(updates.values()) + [existing["id"]]
306
- conn.execute(f"UPDATE followups SET {set_clause} WHERE id = ?", params)
307
- conn.commit()
308
- conn.close()
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": bool(updates),
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
- columns = _table_columns(NEXO_DB, "followups")
516
- payload = {
517
- "id": fid,
518
- "description": description,
519
- "date": date,
520
- "status": "PENDING",
521
- "created_at": now,
522
- "updated_at": now,
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
- conn = sqlite3.connect(str(NEXO_DB))
532
- conn.execute(
533
- f"INSERT OR IGNORE INTO followups ({', '.join(insert_columns)}) VALUES ({', '.join('?' for _ in insert_columns)})",
534
- values,
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
- conn.commit()
537
- conn.close()
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
- ordered_updates = [name for name in update_fields.keys() if name in columns]
262
- if ordered_updates:
263
- assignments = ", ".join(f"{name} = ?" for name in ordered_updates)
264
- conn.execute(
265
- f"UPDATE followups SET {assignments} WHERE id = ?",
266
- [update_fields[name] for name in ordered_updates] + [followup_id],
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
- values = {
271
- "id": followup_id,
272
- "date": "",
273
- "description": description,
274
- "verification": verification,
275
- "status": "PENDING",
276
- "reasoning": reasoning,
277
- "recurrence": None,
278
- "created_at": now_epoch,
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
- now_epoch = datetime.now().timestamp()
326
+ conn.commit()
333
327
  for row in rows:
334
- updates = {"status": "COMPLETED"}
335
- if "updated_at" in columns:
336
- updates["updated_at"] = now_epoch
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("UPDATE followups SET status='COMPLETED' WHERE status LIKE 'COMPLETED %'")
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("UPDATE reminders SET status='COMPLETED' WHERE status LIKE 'COMPLETED %'")
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' (vencidos/hoy), 'all' (todos activos), 'followups' (solo NEXO followups)
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 (4 tools) ──────────────────────────────────────
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 nexo_reminder_update(id: str, description: str = "", date: str = "", status: str = "", category: str = "") -> str:
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 nexo_reminder_delete(id: str) -> str:
547
- """Delete a reminder permanently.
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 (4 tools) ──────────────────────────────────────
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
- result = handle_followup_create(id, description, date, verification, reasoning, recurrence)
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 nexo_followup_update(id: str, description: str = "", date: str = "", verification: str = "", status: str = "", priority: str = "") -> str:
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
- result = handle_followup_update(id, description, date, verification, status)
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 nexo_followup_delete(id: str) -> str:
612
- """Delete a followup permanently.
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) ──────────────────────────────────────
@@ -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, now_epoch() + (max(1, int(review_days)) * 86400), now_epoch(),
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"]
@@ -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' (vencidos/hoy), 'all' (todos activos), 'followups' (solo followups)
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
- lines.append(f" {rid} {fecha_display}{due_marker} {desc[:120]}")
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
- lines.append(f" {nfid} {fecha_display}{due_marker}{pri_tag}{rec_tag} — {desc[:120]}")
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)