nexo-brain 0.8.8 → 0.8.9
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/package.json +1 -1
- package/src/cognitive.py +18 -18
- package/src/db.py +14 -7
- package/src/plugins/agents.py +5 -5
- package/src/plugins/backup.py +2 -2
- package/src/plugins/cognitive_memory.py +8 -8
- package/src/plugins/entities.py +5 -5
- package/src/plugins/episodic_memory.py +39 -39
- package/src/plugins/evolution.py +6 -9
- package/src/plugins/preferences.py +6 -6
- package/src/server.py +6 -6
- package/src/tools_coordination.py +15 -15
- package/src/tools_credentials.py +4 -4
- package/src/tools_learnings.py +4 -4
- package/src/tools_menu.py +51 -33
- package/src/tools_reminders_crud.py +9 -9
- package/src/tools_sessions.py +8 -8
- package/src/tools_task_history.py +7 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.9",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO — Cognitive co-operator for Claude Code. Atkinson-Shiffrin memory, semantic RAG, trust scoring, and metacognitive error prevention.",
|
|
6
6
|
"bin": {
|
package/src/cognitive.py
CHANGED
|
@@ -30,13 +30,13 @@ DISCRIMINATING_ENTITIES = {
|
|
|
30
30
|
# OS / Environment
|
|
31
31
|
"linux", "mac", "macos", "windows", "darwin", "ubuntu", "debian", "alpine",
|
|
32
32
|
# Platforms
|
|
33
|
-
"shopify", "whatsapp", "chrome", "firefox",
|
|
33
|
+
"shopify", "wazion", "canarirural", "recambios", "whatsapp", "chrome", "firefox",
|
|
34
34
|
# Languages / Runtimes
|
|
35
35
|
"python", "php", "javascript", "typescript", "node", "deno", "ruby",
|
|
36
36
|
# Versions
|
|
37
37
|
"v1", "v2", "v3", "v4", "v5", "5.6", "7.4", "8.0", "8.1", "8.2",
|
|
38
38
|
# Infrastructure
|
|
39
|
-
"cloudrun", "gcloud", "vps", "local", "production", "staging",
|
|
39
|
+
"mundiserver", "cloudrun", "gcloud", "vps", "local", "production", "staging",
|
|
40
40
|
# DB
|
|
41
41
|
"mysql", "sqlite", "postgresql", "postgres", "redis",
|
|
42
42
|
}
|
|
@@ -65,12 +65,12 @@ URGENCY_SIGNALS = {
|
|
|
65
65
|
_DEFAULT_TRUST_EVENTS = {
|
|
66
66
|
# Positive
|
|
67
67
|
"explicit_thanks": +3,
|
|
68
|
-
"delegation": +2, #
|
|
69
|
-
"paradigm_shift": +2, #
|
|
68
|
+
"delegation": +2, # Francisco delegates new task without micromanaging
|
|
69
|
+
"paradigm_shift": +2, # Francisco teaches, NEXO learns
|
|
70
70
|
"sibling_detected": +3, # NEXO avoided context error on its own
|
|
71
71
|
"proactive_action": +2, # NEXO did something useful without being asked
|
|
72
72
|
# Negative
|
|
73
|
-
"correction": -3, #
|
|
73
|
+
"correction": -3, # Francisco corrects NEXO
|
|
74
74
|
"repeated_error": -7, # Error on something NEXO already had a learning for
|
|
75
75
|
"override": -5, # NEXO's memory was wrong
|
|
76
76
|
"correction_fatigue": -10, # Same memory corrected 3+ times
|
|
@@ -395,7 +395,7 @@ def _init_tables(conn: sqlite3.Connection):
|
|
|
395
395
|
created_at TEXT DEFAULT (datetime('now'))
|
|
396
396
|
);
|
|
397
397
|
|
|
398
|
-
-- Sentiment readings:
|
|
398
|
+
-- Sentiment readings: Francisco's detected mood per interaction
|
|
399
399
|
CREATE TABLE IF NOT EXISTS sentiment_log (
|
|
400
400
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
401
401
|
sentiment TEXT NOT NULL, -- 'positive', 'negative', 'neutral', 'urgent'
|
|
@@ -421,13 +421,13 @@ def _init_tables(conn: sqlite3.Connection):
|
|
|
421
421
|
status TEXT DEFAULT 'pending'
|
|
422
422
|
);
|
|
423
423
|
|
|
424
|
-
-- Correction tracking: when
|
|
424
|
+
-- Correction tracking: when Francisco overrides a memory's guidance
|
|
425
425
|
CREATE TABLE IF NOT EXISTS memory_corrections (
|
|
426
426
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
427
427
|
memory_id INTEGER NOT NULL,
|
|
428
428
|
store TEXT NOT NULL, -- 'stm' or 'ltm'
|
|
429
429
|
correction_type TEXT NOT NULL, -- 'override', 'exception', 'paradigm_shift'
|
|
430
|
-
context TEXT DEFAULT '', -- what
|
|
430
|
+
context TEXT DEFAULT '', -- what Francisco said
|
|
431
431
|
created_at TEXT DEFAULT (datetime('now'))
|
|
432
432
|
);
|
|
433
433
|
""")
|
|
@@ -2544,12 +2544,12 @@ def get_siblings(memory_id: int) -> list[dict]:
|
|
|
2544
2544
|
def detect_dissonance(new_instruction: str, min_score: float = 0.65) -> list[dict]:
|
|
2545
2545
|
"""Detect cognitive dissonance: find LTM memories that contradict a new instruction.
|
|
2546
2546
|
|
|
2547
|
-
When
|
|
2547
|
+
When Francisco gives a new instruction that conflicts with established LTM memories
|
|
2548
2548
|
(strength > 0.8), this function surfaces the conflict so NEXO can verbalize it
|
|
2549
2549
|
rather than silently obeying or silently resisting.
|
|
2550
2550
|
|
|
2551
2551
|
Args:
|
|
2552
|
-
new_instruction: The new instruction or preference from
|
|
2552
|
+
new_instruction: The new instruction or preference from Francisco
|
|
2553
2553
|
min_score: Minimum cosine similarity to consider as potential conflict
|
|
2554
2554
|
|
|
2555
2555
|
Returns:
|
|
@@ -2584,12 +2584,12 @@ def detect_dissonance(new_instruction: str, min_score: float = 0.65) -> list[dic
|
|
|
2584
2584
|
|
|
2585
2585
|
|
|
2586
2586
|
def resolve_dissonance(memory_id: int, resolution: str, context: str = "") -> str:
|
|
2587
|
-
"""Resolve a cognitive dissonance by applying
|
|
2587
|
+
"""Resolve a cognitive dissonance by applying Francisco's decision.
|
|
2588
2588
|
|
|
2589
2589
|
Args:
|
|
2590
2590
|
memory_id: The LTM memory that conflicts with the new instruction
|
|
2591
2591
|
resolution: One of:
|
|
2592
|
-
- 'paradigm_shift':
|
|
2592
|
+
- 'paradigm_shift': Francisco changed his mind permanently. Decay old memory,
|
|
2593
2593
|
new instruction becomes the standard.
|
|
2594
2594
|
- 'exception': This is a one-time override. Keep old memory as standard.
|
|
2595
2595
|
- 'override': Old memory was wrong. Mark as corrupted and decay to dormant.
|
|
@@ -2640,7 +2640,7 @@ def resolve_dissonance(memory_id: int, resolution: str, context: str = "") -> st
|
|
|
2640
2640
|
def check_correction_fatigue() -> list[dict]:
|
|
2641
2641
|
"""Find memories corrected 3+ times in the last 7 days — mark as 'under review'.
|
|
2642
2642
|
|
|
2643
|
-
These memories are unreliable:
|
|
2643
|
+
These memories are unreliable: Francisco keeps overriding them, suggesting
|
|
2644
2644
|
the memory itself may be wrong or outdated.
|
|
2645
2645
|
|
|
2646
2646
|
Returns:
|
|
@@ -2688,7 +2688,7 @@ def check_correction_fatigue() -> list[dict]:
|
|
|
2688
2688
|
|
|
2689
2689
|
|
|
2690
2690
|
def detect_sentiment(text: str) -> dict:
|
|
2691
|
-
"""Analyze
|
|
2691
|
+
"""Analyze Francisco's text for sentiment signals.
|
|
2692
2692
|
|
|
2693
2693
|
Returns detected sentiment, intensity, and action guidance for NEXO.
|
|
2694
2694
|
Not a model — keyword + heuristic based. Fast and deterministic.
|
|
@@ -2727,13 +2727,13 @@ def detect_sentiment(text: str) -> dict:
|
|
|
2727
2727
|
sentiment = "negative"
|
|
2728
2728
|
intensity = min(1.0, 0.3 + neg_score * 0.15)
|
|
2729
2729
|
if intensity > 0.7:
|
|
2730
|
-
guidance = "MODE: Ultra-
|
|
2730
|
+
guidance = "MODE: Ultra-concise. Zero explanations. Resolve and show result."
|
|
2731
2731
|
else:
|
|
2732
|
-
guidance = "MODE:
|
|
2732
|
+
guidance = "MODE: Concise. Less context, more direct action."
|
|
2733
2733
|
elif pos_score > neg_score and pos_score >= 1:
|
|
2734
2734
|
sentiment = "positive"
|
|
2735
2735
|
intensity = min(1.0, 0.3 + pos_score * 0.15)
|
|
2736
|
-
guidance = "MODE: Normal.
|
|
2736
|
+
guidance = "MODE: Normal. Good moment to propose backlog ideas or improvements."
|
|
2737
2737
|
elif urgency_hits:
|
|
2738
2738
|
sentiment = "urgent"
|
|
2739
2739
|
intensity = 0.8
|
|
@@ -2752,7 +2752,7 @@ def detect_sentiment(text: str) -> dict:
|
|
|
2752
2752
|
|
|
2753
2753
|
|
|
2754
2754
|
def log_sentiment(text: str) -> dict:
|
|
2755
|
-
"""Detect and log
|
|
2755
|
+
"""Detect and log Francisco's sentiment. Returns the detection result."""
|
|
2756
2756
|
result = detect_sentiment(text)
|
|
2757
2757
|
if result["sentiment"] != "neutral":
|
|
2758
2758
|
db = _get_db()
|
package/src/db.py
CHANGED
|
@@ -437,7 +437,7 @@ _SYNONYMS = {
|
|
|
437
437
|
"plantilla": ["template"],
|
|
438
438
|
"template": ["plantilla"],
|
|
439
439
|
"webhook": ["gancho"],
|
|
440
|
-
"cron": ["
|
|
440
|
+
"cron": ["tarea programada", "scheduled"],
|
|
441
441
|
"extension": ["extensión", "plugin", "addon"],
|
|
442
442
|
"plugin": ["extension", "extensión"],
|
|
443
443
|
}
|
|
@@ -1012,24 +1012,31 @@ def register_session(sid: str, task: str) -> dict:
|
|
|
1012
1012
|
return {"sid": sid, "task": task}
|
|
1013
1013
|
|
|
1014
1014
|
|
|
1015
|
-
def update_session(sid: str, task: str) -> dict:
|
|
1016
|
-
"""Update session
|
|
1015
|
+
def update_session(sid: str, task: str | None) -> dict:
|
|
1016
|
+
"""Update session timestamp (and task if provided). Preserves started_epoch.
|
|
1017
|
+
|
|
1018
|
+
Args:
|
|
1019
|
+
sid: Session ID.
|
|
1020
|
+
task: New task description, or None to keep current task (keepalive touch).
|
|
1021
|
+
"""
|
|
1017
1022
|
conn = get_db()
|
|
1018
1023
|
now = now_epoch()
|
|
1019
|
-
row = conn.execute("SELECT started_epoch FROM sessions WHERE sid = ?", (sid,)).fetchone()
|
|
1024
|
+
row = conn.execute("SELECT started_epoch, task FROM sessions WHERE sid = ?", (sid,)).fetchone()
|
|
1020
1025
|
if row:
|
|
1026
|
+
effective_task = task if task is not None else row["task"]
|
|
1021
1027
|
conn.execute(
|
|
1022
1028
|
"UPDATE sessions SET task = ?, last_update_epoch = ?, local_time = ? WHERE sid = ?",
|
|
1023
|
-
(
|
|
1029
|
+
(effective_task, now, local_time_str(), sid)
|
|
1024
1030
|
)
|
|
1025
1031
|
else:
|
|
1032
|
+
effective_task = task or "Unknown"
|
|
1026
1033
|
conn.execute(
|
|
1027
1034
|
"INSERT INTO sessions (sid, task, started_epoch, last_update_epoch, local_time) "
|
|
1028
1035
|
"VALUES (?, ?, ?, ?, ?)",
|
|
1029
|
-
(sid,
|
|
1036
|
+
(sid, effective_task, now, now, local_time_str())
|
|
1030
1037
|
)
|
|
1031
1038
|
conn.commit()
|
|
1032
|
-
return {"sid": sid, "task":
|
|
1039
|
+
return {"sid": sid, "task": effective_task}
|
|
1033
1040
|
|
|
1034
1041
|
|
|
1035
1042
|
def complete_session(sid: str):
|
package/src/plugins/agents.py
CHANGED
|
@@ -5,10 +5,10 @@ def handle_agent_get(id: str) -> str:
|
|
|
5
5
|
"""Get an agent's full profile by ID."""
|
|
6
6
|
a = get_agent(id)
|
|
7
7
|
if not a: return f"Agent '{id}' not found."
|
|
8
|
-
lines = [f"
|
|
8
|
+
lines = [f"AGENT: {a['name']} ({a['id']})", f" Specialization: {a['specialization']}", f" Model: {a['model']}"]
|
|
9
9
|
if a["tools"]: lines.append(f" Tools: {a['tools']}")
|
|
10
|
-
if a["context_files"]: lines.append(f"
|
|
11
|
-
if a["rules"]: lines.append(f"
|
|
10
|
+
if a["context_files"]: lines.append(f" Context: {a['context_files']}")
|
|
11
|
+
if a["rules"]: lines.append(f" Rules: {a['rules']}")
|
|
12
12
|
return "\n".join(lines)
|
|
13
13
|
|
|
14
14
|
def handle_agent_create(id: str, name: str, specialization: str, model: str = "sonnet",
|
|
@@ -31,8 +31,8 @@ def handle_agent_update(id: str, name: str = "", specialization: str = "", model
|
|
|
31
31
|
def handle_agent_list() -> str:
|
|
32
32
|
"""List all registered agents."""
|
|
33
33
|
agents = list_agents()
|
|
34
|
-
if not agents: return "No agents
|
|
35
|
-
lines = ["
|
|
34
|
+
if not agents: return "No registered agents."
|
|
35
|
+
lines = ["REGISTERED AGENTS:"]
|
|
36
36
|
for a in agents:
|
|
37
37
|
lines.append(f" {a['id']} — {a['name']} ({a['model']}) — {a['specialization'][:60]}")
|
|
38
38
|
return "\n".join(lines)
|
package/src/plugins/backup.py
CHANGED
|
@@ -33,10 +33,10 @@ def handle_backup_now() -> str:
|
|
|
33
33
|
def handle_backup_list() -> str:
|
|
34
34
|
"""List available backups with dates and sizes."""
|
|
35
35
|
if not os.path.isdir(BACKUP_DIR):
|
|
36
|
-
return "No backups
|
|
36
|
+
return "No backups."
|
|
37
37
|
files = sorted(glob.glob(os.path.join(BACKUP_DIR, "nexo-*.db")), reverse=True)
|
|
38
38
|
if not files:
|
|
39
|
-
return "No backups
|
|
39
|
+
return "No backups."
|
|
40
40
|
lines = [f"BACKUPS ({len(files)}):"]
|
|
41
41
|
total_size = 0
|
|
42
42
|
for f in files:
|
|
@@ -222,13 +222,13 @@ def handle_cognitive_metrics(days: int = 7) -> str:
|
|
|
222
222
|
|
|
223
223
|
|
|
224
224
|
def handle_cognitive_sentiment(text: str) -> str:
|
|
225
|
-
"""Detect
|
|
225
|
+
"""Detect Francisco's sentiment from his text. Returns mood, intensity, and guidance.
|
|
226
226
|
|
|
227
|
-
Call this with
|
|
227
|
+
Call this with Francisco's recent message to adapt NEXO's tone and behavior.
|
|
228
228
|
Also logs the sentiment for historical tracking.
|
|
229
229
|
|
|
230
230
|
Args:
|
|
231
|
-
text:
|
|
231
|
+
text: Francisco's recent message or instruction
|
|
232
232
|
"""
|
|
233
233
|
result = cognitive.log_sentiment(text)
|
|
234
234
|
trust = cognitive.get_trust_score()
|
|
@@ -294,8 +294,8 @@ def handle_cognitive_trust(event: str = '', context: str = '', delta: float = No
|
|
|
294
294
|
def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
|
|
295
295
|
"""Detect cognitive dissonance: find established memories that conflict with a new instruction.
|
|
296
296
|
|
|
297
|
-
Use BEFORE applying a new preference or rule from
|
|
298
|
-
existing knowledge. If conflicts found, verbalize them and ask
|
|
297
|
+
Use BEFORE applying a new preference or rule from Francisco that might contradict
|
|
298
|
+
existing knowledge. If conflicts found, verbalize them and ask Francisco to resolve.
|
|
299
299
|
|
|
300
300
|
Args:
|
|
301
301
|
instruction: The new instruction or preference to check against LTM
|
|
@@ -328,7 +328,7 @@ def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
|
|
|
328
328
|
lines.append("")
|
|
329
329
|
|
|
330
330
|
lines.append("RESOLVE with nexo_cognitive_resolve, or use force=True to skip:")
|
|
331
|
-
lines.append(" - 'paradigm_shift':
|
|
331
|
+
lines.append(" - 'paradigm_shift': Francisco changed his mind permanently.")
|
|
332
332
|
lines.append(" - 'exception': One-time override. Old memory stays.")
|
|
333
333
|
lines.append(" - 'override': Old memory was wrong.")
|
|
334
334
|
|
|
@@ -336,7 +336,7 @@ def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
|
|
|
336
336
|
|
|
337
337
|
|
|
338
338
|
def handle_cognitive_resolve(memory_id: int, resolution: str, context: str = '') -> str:
|
|
339
|
-
"""Resolve a cognitive dissonance by applying
|
|
339
|
+
"""Resolve a cognitive dissonance by applying Francisco's decision.
|
|
340
340
|
|
|
341
341
|
Args:
|
|
342
342
|
memory_id: The LTM memory ID from the dissonance detection
|
|
@@ -546,7 +546,7 @@ TOOLS = [
|
|
|
546
546
|
(handle_cognitive_metrics, "nexo_cognitive_metrics", "Performance metrics: retrieval relevance %, repeat error rate, multilingual recommendation (spec section 9)"),
|
|
547
547
|
(handle_cognitive_dissonance, "nexo_cognitive_dissonance", "Detect conflicts between a new instruction and established LTM memories. force=True to skip discussion."),
|
|
548
548
|
(handle_cognitive_resolve, "nexo_cognitive_resolve", "Resolve a cognitive dissonance: paradigm_shift, exception, or override."),
|
|
549
|
-
(handle_cognitive_sentiment, "nexo_cognitive_sentiment", "Detect
|
|
549
|
+
(handle_cognitive_sentiment, "nexo_cognitive_sentiment", "Detect Francisco's sentiment and get tone guidance. Also logs for tracking."),
|
|
550
550
|
(handle_cognitive_trust, "nexo_cognitive_trust", "View or adjust trust score (0-100). Without args: view. With event: adjust."),
|
|
551
551
|
(handle_cognitive_pin, "nexo_cognitive_pin", "Pin a memory — never decays, boosted +0.2 in search results."),
|
|
552
552
|
(handle_cognitive_snooze, "nexo_cognitive_snooze", "Snooze a memory — hidden from searches until a date, then auto-restores."),
|
package/src/plugins/entities.py
CHANGED
|
@@ -21,7 +21,7 @@ def handle_entity_create(name: str, type: str, value: str, notes: str = "") -> s
|
|
|
21
21
|
on_entity_create(eid, name, type)
|
|
22
22
|
except Exception:
|
|
23
23
|
pass
|
|
24
|
-
return f"
|
|
24
|
+
return f"Entity created: [{eid}] {name} ({type})"
|
|
25
25
|
|
|
26
26
|
def handle_entity_update(id: int, name: str = "", type: str = "", value: str = "", notes: str = "") -> str:
|
|
27
27
|
"""Update an entity. Only non-empty fields are changed."""
|
|
@@ -30,21 +30,21 @@ def handle_entity_update(id: int, name: str = "", type: str = "", value: str = "
|
|
|
30
30
|
if type: kwargs["type"] = type
|
|
31
31
|
if value: kwargs["value"] = value
|
|
32
32
|
if notes: kwargs["notes"] = notes
|
|
33
|
-
if not kwargs: return "
|
|
33
|
+
if not kwargs: return "Nothing to update."
|
|
34
34
|
update_entity(id, **kwargs)
|
|
35
|
-
return f"
|
|
35
|
+
return f"Entity [{id}] updated."
|
|
36
36
|
|
|
37
37
|
def handle_entity_delete(id: int) -> str:
|
|
38
38
|
"""Delete an entity."""
|
|
39
39
|
if not delete_entity(id):
|
|
40
40
|
return f"ERROR: Entity [{id}] not found."
|
|
41
|
-
return f"
|
|
41
|
+
return f"Entity [{id}] deleted."
|
|
42
42
|
|
|
43
43
|
def handle_entity_list(type: str = "") -> str:
|
|
44
44
|
"""List all entities, optionally filtered by type."""
|
|
45
45
|
results = list_entities(type)
|
|
46
46
|
if not results:
|
|
47
|
-
return "No entities
|
|
47
|
+
return "No entities."
|
|
48
48
|
grouped = {}
|
|
49
49
|
for e in results:
|
|
50
50
|
t = e["type"]
|
|
@@ -27,7 +27,7 @@ def handle_decision_log(domain: str, decision: str, alternatives: str = '',
|
|
|
27
27
|
"""Log a non-trivial decision with reasoning context.
|
|
28
28
|
|
|
29
29
|
Args:
|
|
30
|
-
domain: Area (ads, shopify, server, nexo, other)
|
|
30
|
+
domain: Area (ads, shopify, server, wazion, nexo, canarirural, other)
|
|
31
31
|
decision: What was decided
|
|
32
32
|
alternatives: JSON array or text of options considered and why discarded
|
|
33
33
|
based_on: Data, metrics, or observations that informed this decision
|
|
@@ -35,7 +35,7 @@ def handle_decision_log(domain: str, decision: str, alternatives: str = '',
|
|
|
35
35
|
context_ref: Related followup/reminder ID (e.g., NF-ADS1, R71)
|
|
36
36
|
session_id: Current session ID (auto-filled if empty)
|
|
37
37
|
"""
|
|
38
|
-
valid_domains = {'ads', 'shopify', 'server', 'nexo', 'other'}
|
|
38
|
+
valid_domains = {'ads', 'shopify', 'server', 'wazion', 'nexo', 'canarirural', 'other'}
|
|
39
39
|
if domain not in valid_domains:
|
|
40
40
|
return f"ERROR: domain debe ser uno de: {', '.join(sorted(valid_domains))}"
|
|
41
41
|
if confidence not in ('high', 'medium', 'low'):
|
|
@@ -65,7 +65,7 @@ def handle_decision_log(domain: str, decision: str, alternatives: str = '',
|
|
|
65
65
|
result = dict(conn.execute("SELECT * FROM decisions WHERE id = ?", (result["id"],)).fetchone())
|
|
66
66
|
due = result.get("review_due_at", "")
|
|
67
67
|
due_str = f" review_due={due}" if due else ""
|
|
68
|
-
return f"Decision #{result['id']}
|
|
68
|
+
return f"Decision #{result['id']} logged [{domain}] ({confidence}): {decision[:80]}{due_str}"
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def handle_decision_outcome(id: int, outcome: str) -> str:
|
|
@@ -84,7 +84,7 @@ def handle_decision_outcome(id: int, outcome: str) -> str:
|
|
|
84
84
|
(id,)
|
|
85
85
|
)
|
|
86
86
|
conn.commit()
|
|
87
|
-
return f"Decision #{id} outcome
|
|
87
|
+
return f"Decision #{id} outcome recorded: {outcome[:100]}"
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
def handle_decision_search(query: str = '', domain: str = '', days: int = 30) -> str:
|
|
@@ -92,16 +92,16 @@ def handle_decision_search(query: str = '', domain: str = '', days: int = 30) ->
|
|
|
92
92
|
|
|
93
93
|
Args:
|
|
94
94
|
query: Text to search in decision, alternatives, based_on, outcome
|
|
95
|
-
domain: Filter by area (ads, shopify, server, nexo, other)
|
|
95
|
+
domain: Filter by area (ads, shopify, server, wazion, nexo, canarirural, other)
|
|
96
96
|
days: Look back N days (default 30)
|
|
97
97
|
"""
|
|
98
|
-
valid_domains = {'ads', 'shopify', 'server', 'nexo', 'other'}
|
|
98
|
+
valid_domains = {'ads', 'shopify', 'server', 'wazion', 'nexo', 'canarirural', 'other'}
|
|
99
99
|
if domain and domain not in valid_domains:
|
|
100
100
|
return f"ERROR: domain debe ser uno de: {', '.join(sorted(valid_domains))}"
|
|
101
101
|
results = search_decisions(query, domain, days)
|
|
102
102
|
if not results:
|
|
103
103
|
scope = f"'{query}'" if query else domain or 'todas'
|
|
104
|
-
return f"
|
|
104
|
+
return f"No decisions found for {scope} in {days} days."
|
|
105
105
|
|
|
106
106
|
lines = [f"DECISIONES ({len(results)}):"]
|
|
107
107
|
for d in results:
|
|
@@ -113,9 +113,9 @@ def handle_decision_search(query: str = '', domain: str = '', days: int = 30) ->
|
|
|
113
113
|
lines.append(f" #{d['id']} ({d['created_at']}) [{d['domain']}] {conf} [{status}]{ref}{review_due}")
|
|
114
114
|
lines.append(f" {d['decision'][:120]}")
|
|
115
115
|
if d.get('based_on'):
|
|
116
|
-
lines.append(f"
|
|
116
|
+
lines.append(f" Based on: {d['based_on'][:100]}")
|
|
117
117
|
if d.get('alternatives'):
|
|
118
|
-
lines.append(f"
|
|
118
|
+
lines.append(f" Alternatives: {d['alternatives'][:100]}")
|
|
119
119
|
if outcome_str:
|
|
120
120
|
lines.append(f" Outcome:{outcome_str}")
|
|
121
121
|
return "\n".join(lines)
|
|
@@ -163,7 +163,7 @@ def handle_memory_review_queue(days: int = 0) -> str:
|
|
|
163
163
|
def handle_session_diary_write(decisions: str, summary: str,
|
|
164
164
|
discarded: str = '', pending: str = '',
|
|
165
165
|
context_next: str = '', mental_state: str = '',
|
|
166
|
-
|
|
166
|
+
francisco_signals: str = '',
|
|
167
167
|
domain: str = '',
|
|
168
168
|
session_id: str = '',
|
|
169
169
|
self_critique: str = '') -> str:
|
|
@@ -176,16 +176,16 @@ def handle_session_diary_write(decisions: str, summary: str,
|
|
|
176
176
|
pending: Items left unresolved, with doubt level
|
|
177
177
|
context_next: What the next session should know to continue effectively
|
|
178
178
|
mental_state: Internal state to transfer — thread of thought, tone, observations not yet shared, momentum. Written in first person as NEXO.
|
|
179
|
-
|
|
180
|
-
domain: Project context:
|
|
179
|
+
francisco_signals: Observable signals from Francisco during session — response speed (fast='s' vs detailed explanations), tone (direct, frustrated, exploratory, excited), corrections given, topics he initiated vs topics NEXO initiated. Factual observations only, not interpretations.
|
|
180
|
+
domain: Project context: recambios, wazion, nexo, canarirural, server, other
|
|
181
181
|
session_id: Current session ID
|
|
182
|
-
self_critique:
|
|
182
|
+
self_critique: MANDATORY. Honest post-mortem: What should I have done proactively? Did Francisco ask for something I should have detected? Did I repeat known errors? What concrete rule would prevent the recurrence? If clean session: 'No self-critique — clean session.'
|
|
183
183
|
"""
|
|
184
184
|
sid = session_id or 'unknown'
|
|
185
185
|
# Clean up draft — manual diary supersedes it
|
|
186
186
|
from db import delete_diary_draft
|
|
187
187
|
delete_diary_draft(sid)
|
|
188
|
-
result = write_session_diary(sid, decisions, summary, discarded, pending, context_next, mental_state, domain=domain, user_signals=
|
|
188
|
+
result = write_session_diary(sid, decisions, summary, discarded, pending, context_next, mental_state, domain=domain, user_signals=francisco_signals, self_critique=self_critique)
|
|
189
189
|
if "error" in result:
|
|
190
190
|
return f"ERROR: {result['error']}"
|
|
191
191
|
_cognitive_ingest_safe(summary, "diary", f"diary#{result.get('id','')}", f"Session {sid} summary", domain)
|
|
@@ -194,7 +194,7 @@ def handle_session_diary_write(decisions: str, summary: str,
|
|
|
194
194
|
if mental_state and mental_state.strip():
|
|
195
195
|
_cognitive_ingest_safe(mental_state, "mental_state", f"diary#{result.get('id','')}", f"Session {sid} state", domain)
|
|
196
196
|
domain_str = f" [{domain}]" if domain else ""
|
|
197
|
-
msg = f"
|
|
197
|
+
msg = f"Session diary #{result['id']}{domain_str} saved: {summary[:80]}"
|
|
198
198
|
|
|
199
199
|
# Trust score & sentiment summary for session diary
|
|
200
200
|
try:
|
|
@@ -215,14 +215,14 @@ def handle_session_diary_write(decisions: str, summary: str,
|
|
|
215
215
|
"SELECT COUNT(*) FROM change_log WHERE (commit_ref IS NULL OR commit_ref = '')"
|
|
216
216
|
).fetchone()[0]
|
|
217
217
|
if orphan_changes > 0:
|
|
218
|
-
warnings.append(f"{orphan_changes} changes
|
|
218
|
+
warnings.append(f"{orphan_changes} changes without commit_ref")
|
|
219
219
|
orphan_decisions = conn.execute(
|
|
220
220
|
"SELECT COUNT(*) FROM decisions WHERE (outcome IS NULL OR outcome = '') AND created_at < datetime('now', '-7 days')"
|
|
221
221
|
).fetchone()[0]
|
|
222
222
|
if orphan_decisions > 0:
|
|
223
|
-
warnings.append(f"{orphan_decisions} decisions >7d
|
|
223
|
+
warnings.append(f"{orphan_decisions} decisions >7d without outcome")
|
|
224
224
|
if warnings:
|
|
225
|
-
msg += "\n⚠ EPISODIC GAPS: " + " | ".join(warnings) + " —
|
|
225
|
+
msg += "\n⚠ EPISODIC GAPS: " + " | ".join(warnings) + " — resolve before closing session."
|
|
226
226
|
|
|
227
227
|
return msg
|
|
228
228
|
|
|
@@ -235,29 +235,29 @@ def handle_session_diary_read(session_id: str = '', last_n: int = 3, last_day: b
|
|
|
235
235
|
session_id: Specific session ID to read (optional)
|
|
236
236
|
last_n: Number of recent entries to return (default 3)
|
|
237
237
|
last_day: If true, returns ALL entries from the most recent day (multi-terminal aware). Use this at startup.
|
|
238
|
-
domain: Filter by project context:
|
|
238
|
+
domain: Filter by project context: recambios, wazion, nexo, canarirural, server, other
|
|
239
239
|
"""
|
|
240
240
|
results = read_session_diary(session_id, last_n, last_day, domain)
|
|
241
241
|
if not results:
|
|
242
|
-
return "
|
|
242
|
+
return "No entries in the session diary."
|
|
243
243
|
|
|
244
|
-
lines = [f"
|
|
244
|
+
lines = [f"SESSION DIARY ({len(results)}):"]
|
|
245
245
|
for d in results:
|
|
246
246
|
domain_label = f" [{d['domain']}]" if d.get('domain') else ""
|
|
247
|
-
lines.append(f"\n ---
|
|
248
|
-
lines.append(f"
|
|
247
|
+
lines.append(f"\n --- Session {d['session_id']}{domain_label} ({d['created_at']}) ---")
|
|
248
|
+
lines.append(f" Summary: {d['summary']}")
|
|
249
249
|
if d.get('decisions'):
|
|
250
|
-
lines.append(f"
|
|
250
|
+
lines.append(f" Decisions: {d['decisions'][:200]}")
|
|
251
251
|
if d.get('discarded'):
|
|
252
|
-
lines.append(f"
|
|
252
|
+
lines.append(f" Discarded: {d['discarded'][:150]}")
|
|
253
253
|
if d.get('pending'):
|
|
254
|
-
lines.append(f"
|
|
254
|
+
lines.append(f" Pending: {d['pending'][:150]}")
|
|
255
255
|
if d.get('context_next'):
|
|
256
|
-
lines.append(f"
|
|
256
|
+
lines.append(f" For next session: {d['context_next'][:200]}")
|
|
257
257
|
if d.get('mental_state'):
|
|
258
|
-
lines.append(f"
|
|
259
|
-
if d.get('
|
|
260
|
-
lines.append(f"
|
|
258
|
+
lines.append(f" Mental state: {d['mental_state'][:300]}")
|
|
259
|
+
if d.get('francisco_signals'):
|
|
260
|
+
lines.append(f" Francisco signals: {d['francisco_signals'][:300]}")
|
|
261
261
|
return "\n".join(lines)
|
|
262
262
|
|
|
263
263
|
|
|
@@ -271,7 +271,7 @@ def handle_change_log(files: str, what_changed: str, why: str,
|
|
|
271
271
|
files: File path(s) modified (comma-separated if multiple)
|
|
272
272
|
what_changed: What was modified — functions, lines, behavior change
|
|
273
273
|
why: WHY this change was needed — the root cause, not just "fix bug"
|
|
274
|
-
triggered_by: What triggered this — bug report, metric,
|
|
274
|
+
triggered_by: What triggered this — bug report, metric, Francisco's request, followup ID
|
|
275
275
|
affects: What systems/users/flows this change impacts
|
|
276
276
|
risks: What could go wrong — regressions, edge cases, dependencies
|
|
277
277
|
verify: How to verify this works — what to check, followup ID if created
|
|
@@ -295,9 +295,9 @@ def handle_change_log(files: str, what_changed: str, why: str,
|
|
|
295
295
|
on_change_log(change_id, files, "")
|
|
296
296
|
except Exception:
|
|
297
297
|
pass
|
|
298
|
-
msg = f"Change #{change_id}
|
|
298
|
+
msg = f"Change #{change_id} logged: {files[:60]} — {what_changed[:60]}"
|
|
299
299
|
if not commit_ref:
|
|
300
|
-
msg += f"\n⚠
|
|
300
|
+
msg += f"\n⚠ NO COMMIT. Use nexo_change_commit({change_id}, 'hash') after push, or 'server-direct' if it was a direct server edit."
|
|
301
301
|
return msg
|
|
302
302
|
|
|
303
303
|
|
|
@@ -319,8 +319,8 @@ def handle_change_search(query: str = '', files: str = '', days: int = 30) -> st
|
|
|
319
319
|
commit = f" [{c['commit_ref'][:8]}]" if c.get('commit_ref') else ""
|
|
320
320
|
lines.append(f" #{c['id']} ({c['created_at']}){commit}")
|
|
321
321
|
lines.append(f" Archivos: {c['files'][:100]}")
|
|
322
|
-
lines.append(f"
|
|
323
|
-
lines.append(f"
|
|
322
|
+
lines.append(f" What: {c['what_changed'][:120]}")
|
|
323
|
+
lines.append(f" Why: {c['why'][:120]}")
|
|
324
324
|
if c.get('triggered_by'):
|
|
325
325
|
lines.append(f" Trigger: {c['triggered_by'][:80]}")
|
|
326
326
|
if c.get('affects'):
|
|
@@ -352,7 +352,7 @@ def handle_recall(query: str, days: int = 30) -> str:
|
|
|
352
352
|
"""
|
|
353
353
|
results = recall(query, days)
|
|
354
354
|
if not results:
|
|
355
|
-
return f"
|
|
355
|
+
return f"No results for '{query}' in the last {days} days."
|
|
356
356
|
|
|
357
357
|
# v1.2: Passive rehearsal — strengthen matching cognitive memories
|
|
358
358
|
try:
|
|
@@ -367,13 +367,13 @@ def handle_recall(query: str, days: int = 30) -> str:
|
|
|
367
367
|
SOURCE_LABELS = {
|
|
368
368
|
'change_log': '[CAMBIO]',
|
|
369
369
|
'change': '[CAMBIO]',
|
|
370
|
-
'decision': '[
|
|
370
|
+
'decision': '[DECISION]',
|
|
371
371
|
'learning': '[LEARNING]',
|
|
372
372
|
'followup': '[FOLLOWUP]',
|
|
373
373
|
'diary': '[DIARIO]',
|
|
374
374
|
'entity': '[ENTIDAD]',
|
|
375
375
|
'file': '[ARCHIVO]',
|
|
376
|
-
'code': '[
|
|
376
|
+
'code': '[CODE]',
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
lines = [f"RECALL '{query}' — {len(results)} resultado(s):"]
|
|
@@ -390,7 +390,7 @@ def handle_recall(query: str, days: int = 30) -> str:
|
|
|
390
390
|
if snippet:
|
|
391
391
|
lines.append(f" {snippet}")
|
|
392
392
|
if len(results) < 5:
|
|
393
|
-
lines.append(f"\n 💡
|
|
393
|
+
lines.append(f"\n 💡 Only {len(results)} results in NEXO. For deeper history, also search claude-mem: mcp__plugin_claude-mem_mcp-search__search")
|
|
394
394
|
return "\n".join(lines)
|
|
395
395
|
|
|
396
396
|
|
package/src/plugins/evolution.py
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
"""Evolution plugin — NEXO self-improvement tools for interactive sessions."""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
3
|
from db import get_latest_metrics, get_evolution_history, update_evolution_log_status, get_db
|
|
6
4
|
|
|
7
|
-
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
8
|
-
|
|
9
5
|
|
|
10
6
|
def handle_evolution_status() -> str:
|
|
11
7
|
"""Show current NEXO dimension scores and recent trend."""
|
|
@@ -61,7 +57,8 @@ def handle_evolution_propose() -> str:
|
|
|
61
57
|
This sets a flag that the Cortex wrapper reads on the next cycle.
|
|
62
58
|
"""
|
|
63
59
|
import json
|
|
64
|
-
|
|
60
|
+
from pathlib import Path
|
|
61
|
+
obj_file = Path.home() / "claude" / "cortex" / "evolution-objective.json"
|
|
65
62
|
if not obj_file.exists():
|
|
66
63
|
return "ERROR: evolution-objective.json not found"
|
|
67
64
|
try:
|
|
@@ -80,10 +77,10 @@ def handle_evolution_approve(log_id: int, notes: str = '') -> str:
|
|
|
80
77
|
|
|
81
78
|
Args:
|
|
82
79
|
log_id: Evolution log entry ID to approve
|
|
83
|
-
notes: Optional notes
|
|
80
|
+
notes: Optional notes from Francisco
|
|
84
81
|
"""
|
|
85
82
|
update_evolution_log_status(log_id, "accepted",
|
|
86
|
-
test_result=f"Approved. {notes}".strip())
|
|
83
|
+
test_result=f"Approved by Francisco. {notes}".strip())
|
|
87
84
|
return f"Proposal #{log_id} APPROVED. Will be applied in next Evolution cycle."
|
|
88
85
|
|
|
89
86
|
|
|
@@ -95,7 +92,7 @@ def handle_evolution_reject(log_id: int, reason: str = '') -> str:
|
|
|
95
92
|
reason: Why this proposal was rejected
|
|
96
93
|
"""
|
|
97
94
|
update_evolution_log_status(log_id, "rejected",
|
|
98
|
-
test_result=f"Rejected: {reason}" if reason else "Rejected by
|
|
95
|
+
test_result=f"Rejected: {reason}" if reason else "Rejected by Francisco")
|
|
99
96
|
return f"Proposal #{log_id} REJECTED. Reason: {reason or 'no reason given'}"
|
|
100
97
|
|
|
101
98
|
|
|
@@ -107,7 +104,7 @@ TOOLS = [
|
|
|
107
104
|
(handle_evolution_propose, "nexo_evolution_propose",
|
|
108
105
|
"Manually trigger an evolution analysis outside weekly schedule"),
|
|
109
106
|
(handle_evolution_approve, "nexo_evolution_approve",
|
|
110
|
-
"Approve a pending Evolution proposal"),
|
|
107
|
+
"Approve a pending Evolution proposal (Francisco only)"),
|
|
111
108
|
(handle_evolution_reject, "nexo_evolution_reject",
|
|
112
109
|
"Reject a pending Evolution proposal with reason"),
|
|
113
110
|
]
|
|
@@ -4,7 +4,7 @@ from db import set_preference, get_preference, list_preferences, delete_preferen
|
|
|
4
4
|
def handle_preference_get(key: str) -> str:
|
|
5
5
|
"""Get a specific preference by key."""
|
|
6
6
|
p = get_preference(key)
|
|
7
|
-
if not p: return f"
|
|
7
|
+
if not p: return f"Preference '{key}' not found."
|
|
8
8
|
return f"{p['key']} = {p['value']} (cat: {p['category']})"
|
|
9
9
|
|
|
10
10
|
def handle_preference_set(key: str, value: str, category: str = "general") -> str:
|
|
@@ -15,18 +15,18 @@ def handle_preference_set(key: str, value: str, category: str = "general") -> st
|
|
|
15
15
|
cognitive.ingest_to_ltm(f"{key}: {value}", "preference", key, key, "")
|
|
16
16
|
except Exception:
|
|
17
17
|
pass
|
|
18
|
-
return f"
|
|
18
|
+
return f"Preference '{key}' = '{value}' ({category})"
|
|
19
19
|
|
|
20
20
|
def handle_preference_list(category: str = "") -> str:
|
|
21
21
|
"""List all preferences, optionally filtered by category."""
|
|
22
22
|
prefs = list_preferences(category)
|
|
23
|
-
if not prefs: return "
|
|
23
|
+
if not prefs: return "No preferences."
|
|
24
24
|
grouped = {}
|
|
25
25
|
for p in prefs:
|
|
26
26
|
c = p["category"]
|
|
27
27
|
if c not in grouped: grouped[c] = []
|
|
28
28
|
grouped[c].append(p)
|
|
29
|
-
lines = ["
|
|
29
|
+
lines = ["PREFERENCES:"]
|
|
30
30
|
for c, items in grouped.items():
|
|
31
31
|
lines.append(f"\n [{c.upper()}]")
|
|
32
32
|
for p in items:
|
|
@@ -36,8 +36,8 @@ def handle_preference_list(category: str = "") -> str:
|
|
|
36
36
|
def handle_preference_delete(key: str) -> str:
|
|
37
37
|
"""Delete a preference."""
|
|
38
38
|
if not delete_preference(key):
|
|
39
|
-
return f"ERROR:
|
|
40
|
-
return f"
|
|
39
|
+
return f"ERROR: Preference '{key}' not found."
|
|
40
|
+
return f"Preference '{key}' deleted."
|
|
41
41
|
|
|
42
42
|
TOOLS = [
|
|
43
43
|
(handle_preference_get, "nexo_preference_get", "Get a specific preference value"),
|
package/src/server.py
CHANGED
|
@@ -52,7 +52,7 @@ mcp = FastMCP(
|
|
|
52
52
|
name="nexo",
|
|
53
53
|
instructions=(
|
|
54
54
|
"NEXO operational server. Provides session coordination, "
|
|
55
|
-
"reminders, followups, and menu for
|
|
55
|
+
"reminders, followups, and menu for Francisco's operations.\n\n"
|
|
56
56
|
"When working with tool results, write down any important information "
|
|
57
57
|
"you might need later in your response, as the original tool result "
|
|
58
58
|
"may be cleared later."
|
|
@@ -208,13 +208,13 @@ def nexo_menu() -> str:
|
|
|
208
208
|
|
|
209
209
|
@mcp.tool
|
|
210
210
|
def nexo_reminder_create(id: str, description: str, date: str = "", category: str = "general") -> str:
|
|
211
|
-
"""Create a new reminder for
|
|
211
|
+
"""Create a new reminder for Francisco.
|
|
212
212
|
|
|
213
213
|
Args:
|
|
214
214
|
id: Unique ID starting with 'R' (e.g., R90).
|
|
215
215
|
description: What needs to be done.
|
|
216
216
|
date: Target date YYYY-MM-DD (optional).
|
|
217
|
-
category: One of:
|
|
217
|
+
category: One of: decisiones, tareas, esperando, ideas, general.
|
|
218
218
|
"""
|
|
219
219
|
return handle_reminder_create(id, description, date, category)
|
|
220
220
|
|
|
@@ -313,7 +313,7 @@ def nexo_learning_add(category: str, title: str, content: str, reasoning: str =
|
|
|
313
313
|
"""Add a new learning (resolved error, pattern, gotcha).
|
|
314
314
|
|
|
315
315
|
Args:
|
|
316
|
-
category: One of: nexo-ops, infrastructure, security, brain-engine
|
|
316
|
+
category: One of: nexo-ops, google-ads, meta-ads, google-analytics, shopify, wazion, cloud-sql, infrastructure, security, brain-engine.
|
|
317
317
|
title: Short title for the learning.
|
|
318
318
|
content: Full description with context and solution.
|
|
319
319
|
reasoning: WHY this matters — what led to discovering this (optional).
|
|
@@ -417,7 +417,7 @@ def nexo_index_dirs() -> str:
|
|
|
417
417
|
dirs = fts_list_dirs()
|
|
418
418
|
if not dirs:
|
|
419
419
|
return "No directories configured."
|
|
420
|
-
lines = ["
|
|
420
|
+
lines = ["INDEXED DIRECTORIES:"]
|
|
421
421
|
for d in dirs:
|
|
422
422
|
source_tag = "⚙️" if d["source"] == "builtin" else "➕"
|
|
423
423
|
notes = f" — {d['notes']}" if d.get("notes") else ""
|
|
@@ -544,7 +544,7 @@ def nexo_plugin_list() -> str:
|
|
|
544
544
|
plugins = list_plugins()
|
|
545
545
|
if not plugins:
|
|
546
546
|
return "No plugins loaded."
|
|
547
|
-
lines = ["PLUGINS
|
|
547
|
+
lines = ["LOADED PLUGINS:"]
|
|
548
548
|
for p in plugins:
|
|
549
549
|
names = p["tool_names"] or "(no tools)"
|
|
550
550
|
lines.append(f" {p['filename']} — {p['tools_count']} tools: {names}")
|
|
@@ -19,13 +19,13 @@ def handle_track(sid: str, paths: list[str]) -> str:
|
|
|
19
19
|
|
|
20
20
|
if result["conflicts"]:
|
|
21
21
|
lines.append("")
|
|
22
|
-
lines.append("
|
|
22
|
+
lines.append("CONFLICTO DE ARCHIVOS:")
|
|
23
23
|
for c in result["conflicts"]:
|
|
24
24
|
lines.append(f" {c['sid']} ({c['task']}):")
|
|
25
25
|
for f in c["files"]:
|
|
26
26
|
lines.append(f" {f}")
|
|
27
27
|
lines.append("")
|
|
28
|
-
lines.append("
|
|
28
|
+
lines.append("PARAR e informar a Francisco antes de editar.")
|
|
29
29
|
|
|
30
30
|
return "\n".join(lines)
|
|
31
31
|
|
|
@@ -35,7 +35,7 @@ def handle_untrack(sid: str, paths: list[str] | None = None) -> str:
|
|
|
35
35
|
untrack_files(sid, paths)
|
|
36
36
|
if paths:
|
|
37
37
|
return f"Untracked: {', '.join(paths)}"
|
|
38
|
-
return "
|
|
38
|
+
return "Todos los archivos liberados."
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def handle_files() -> str:
|
|
@@ -56,7 +56,7 @@ def handle_files() -> str:
|
|
|
56
56
|
conflicts = {p: sids for p, sids in all_paths.items() if len(sids) > 1}
|
|
57
57
|
if conflicts:
|
|
58
58
|
lines.append("")
|
|
59
|
-
lines.append("
|
|
59
|
+
lines.append("CONFLICTOS:")
|
|
60
60
|
for path, sids in conflicts.items():
|
|
61
61
|
lines.append(f" {path} -> {', '.join(sids)}")
|
|
62
62
|
|
|
@@ -66,19 +66,19 @@ def handle_files() -> str:
|
|
|
66
66
|
def handle_send(from_sid: str, to_sid: str, text: str) -> str:
|
|
67
67
|
"""Send a message. to_sid='all' for broadcast."""
|
|
68
68
|
msg_id = send_message(from_sid, to_sid, text)
|
|
69
|
-
target = "
|
|
70
|
-
return f"
|
|
69
|
+
target = "todas las sesiones" if to_sid == "all" else to_sid
|
|
70
|
+
return f"Mensaje {msg_id} enviado a {target}."
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
def handle_ask(from_sid: str, to_sid: str, question: str) -> str:
|
|
74
74
|
"""Create a question to another session (non-blocking)."""
|
|
75
75
|
qid = ask_question(from_sid, to_sid, question)
|
|
76
76
|
return (
|
|
77
|
-
f"
|
|
78
|
-
f"
|
|
79
|
-
f"
|
|
80
|
-
f"
|
|
81
|
-
f"
|
|
77
|
+
f"Pregunta enviada: {qid}\n"
|
|
78
|
+
f"Para: {to_sid}\n"
|
|
79
|
+
f"Pregunta: {question}\n\n"
|
|
80
|
+
f"La otra sesion vera la pregunta en su proximo nexo_heartbeat.\n"
|
|
81
|
+
f"Usa nexo_check_answer(qid='{qid}') para ver si respondieron."
|
|
82
82
|
)
|
|
83
83
|
|
|
84
84
|
|
|
@@ -87,7 +87,7 @@ def handle_answer(qid: str, answer_text: str) -> str:
|
|
|
87
87
|
result = answer_question(qid, answer_text)
|
|
88
88
|
if "error" in result:
|
|
89
89
|
return f"ERROR: {result['error']}"
|
|
90
|
-
return f"
|
|
90
|
+
return f"Respondido {qid}: {answer_text}"
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
def handle_check_answer(qid: str) -> str:
|
|
@@ -96,7 +96,7 @@ def handle_check_answer(qid: str) -> str:
|
|
|
96
96
|
if not result:
|
|
97
97
|
return f"Question {qid} not found."
|
|
98
98
|
if result["status"] == "answered":
|
|
99
|
-
return f"
|
|
99
|
+
return f"RESPUESTA de {qid}: {result['answer']}"
|
|
100
100
|
elif result["status"] == "expired":
|
|
101
|
-
return f"
|
|
102
|
-
return f"
|
|
101
|
+
return f"Pregunta {qid} expirada sin respuesta."
|
|
102
|
+
return f"Pregunta {qid} sigue pendiente. Reintentar en unos segundos."
|
package/src/tools_credentials.py
CHANGED
|
@@ -11,10 +11,10 @@ def handle_credential_get(service: str, key: str = '') -> str:
|
|
|
11
11
|
return f"ERROR: No credentials found for '{target}'."
|
|
12
12
|
lines = []
|
|
13
13
|
for r in results:
|
|
14
|
-
lines.append(f"
|
|
15
|
-
lines.append(f"
|
|
14
|
+
lines.append(f"CREDENTIAL {r['service']}/{r['key']}:")
|
|
15
|
+
lines.append(f" Value: {r['value']}")
|
|
16
16
|
notes = r.get("notes") or ""
|
|
17
|
-
lines.append(f"
|
|
17
|
+
lines.append(f" Notes: {notes if notes else '—'}")
|
|
18
18
|
return "\n".join(lines)
|
|
19
19
|
|
|
20
20
|
|
|
@@ -53,7 +53,7 @@ def handle_credential_delete(service: str, key: str = '') -> str:
|
|
|
53
53
|
def handle_credential_list(service: str = '') -> str:
|
|
54
54
|
"""List credential service/key names and notes — values are never shown."""
|
|
55
55
|
results = list_credentials(service if service else None)
|
|
56
|
-
label = service if service else "
|
|
56
|
+
label = service if service else "ALL"
|
|
57
57
|
if not results:
|
|
58
58
|
return f"CREDENTIALS {label.upper()}: No entries."
|
|
59
59
|
lines = [f"CREDENTIALS {label.upper()} ({len(results)}):"]
|
package/src/tools_learnings.py
CHANGED
|
@@ -100,7 +100,7 @@ def handle_learning_add(category: str, title: str, content: str, reasoning: str
|
|
|
100
100
|
if applies_to:
|
|
101
101
|
meta.append(f"applies_to={applies_to}")
|
|
102
102
|
meta_str = f" ({', '.join(meta)})" if meta else ""
|
|
103
|
-
return f"Learning #{result['id']}
|
|
103
|
+
return f"Learning #{result['id']} added to {category}: {title}{meta_str}{repetition_msg}"
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
def handle_learning_search(query: str, category: str = '') -> str:
|
|
@@ -117,7 +117,7 @@ def handle_learning_search(query: str, category: str = '') -> str:
|
|
|
117
117
|
lines.append(f" #{r['id']} [{r['category']}] [{status}] {r['title']}{review_note}")
|
|
118
118
|
lines.append(f" {snippet}")
|
|
119
119
|
if r.get("prevention"):
|
|
120
|
-
lines.append(f"
|
|
120
|
+
lines.append(f" Prevention: {r['prevention'][:100]}")
|
|
121
121
|
|
|
122
122
|
# v1.2: Passive rehearsal — strengthen matching cognitive memories
|
|
123
123
|
try:
|
|
@@ -176,7 +176,7 @@ def handle_learning_update(id: int, title: str = '', content: str = '', category
|
|
|
176
176
|
conn = get_db()
|
|
177
177
|
conn.execute(f"UPDATE learnings SET {set_clause} WHERE id = ?", values)
|
|
178
178
|
conn.commit()
|
|
179
|
-
return f"Learning #{id}
|
|
179
|
+
return f"Learning #{id} updated."
|
|
180
180
|
|
|
181
181
|
|
|
182
182
|
def handle_learning_delete(id: int) -> str:
|
|
@@ -184,7 +184,7 @@ def handle_learning_delete(id: int) -> str:
|
|
|
184
184
|
deleted = delete_learning(id)
|
|
185
185
|
if not deleted:
|
|
186
186
|
return f"ERROR: Learning #{id} not found."
|
|
187
|
-
return f"Learning #{id}
|
|
187
|
+
return f"Learning #{id} deleted."
|
|
188
188
|
|
|
189
189
|
|
|
190
190
|
def handle_learning_list(category: str = '') -> str:
|
package/src/tools_menu.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from datetime import datetime, timedelta
|
|
4
4
|
import json
|
|
5
|
-
import os
|
|
6
5
|
import subprocess
|
|
7
6
|
import sys
|
|
8
7
|
from pathlib import Path
|
|
@@ -10,16 +9,14 @@ from tools_sessions import handle_status
|
|
|
10
9
|
from tools_reminders import handle_reminders
|
|
11
10
|
from db import get_db
|
|
12
11
|
|
|
13
|
-
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
14
|
-
|
|
15
12
|
|
|
16
13
|
def _get_date_str() -> str:
|
|
17
|
-
"""Get formatted
|
|
14
|
+
"""Get formatted date in Madrid timezone."""
|
|
18
15
|
try:
|
|
19
16
|
result = subprocess.run(
|
|
20
|
-
["date", "+%A %d %B %Y, %H:%M"],
|
|
17
|
+
["date", "+%A %d de %B de %Y, %H:%M"],
|
|
21
18
|
capture_output=True, text=True,
|
|
22
|
-
env={"PATH": "/usr/bin:/bin"}
|
|
19
|
+
env={"TZ": "Europe/Madrid", "PATH": "/usr/bin:/bin", "LANG": "es_ES.UTF-8"}
|
|
23
20
|
)
|
|
24
21
|
return result.stdout.strip()
|
|
25
22
|
except Exception:
|
|
@@ -27,21 +24,39 @@ def _get_date_str() -> str:
|
|
|
27
24
|
|
|
28
25
|
|
|
29
26
|
MENU_ITEMS = [
|
|
30
|
-
("
|
|
31
|
-
("1", "
|
|
32
|
-
("
|
|
27
|
+
("Proyectos", [
|
|
28
|
+
("1", "WAzion - Revisar estado del proyecto"),
|
|
29
|
+
("9", "Claude Agent VPS - Revisar cambios autonomos"),
|
|
30
|
+
]),
|
|
31
|
+
("Publicidad", [
|
|
32
|
+
("7", "Google Ads - Administrar campanas (Recambios + WAzion)"),
|
|
33
|
+
("7b", "Meta Ads - Administrar campanas Facebook/Instagram"),
|
|
34
|
+
("7c", "Ads Tracking - Revision combinada Google+Meta"),
|
|
35
|
+
]),
|
|
36
|
+
("Shopify", [
|
|
37
|
+
("4", "Shopify Theme Sync - Sincronizar tema"),
|
|
38
|
+
("5", "Shopify Scripts - Ejecutar scripts periodicos"),
|
|
39
|
+
("6", "Cambiar Promocion Shopify"),
|
|
33
40
|
]),
|
|
34
|
-
("
|
|
35
|
-
("
|
|
36
|
-
("
|
|
41
|
+
("Servidor e Infraestructura", [
|
|
42
|
+
("2", "Servidor - Chequeo cl105e.mundiserver.com"),
|
|
43
|
+
("3", "WhatsApp Logs - Revisar logs vicshopsysteam"),
|
|
44
|
+
("11", "File Tracker - Reporte archivos PHP"),
|
|
45
|
+
("12", "Google Cloud - Gasto, consumo y estado GCP"),
|
|
37
46
|
]),
|
|
38
|
-
("
|
|
39
|
-
("
|
|
40
|
-
("
|
|
47
|
+
("Comunicacion y Monitorizacion", [
|
|
48
|
+
("8", "Recovery Optimizer - Analisis IA semanal (LUNES)"),
|
|
49
|
+
("10", "Recovery Monitor - Estado emails/WA recovery (24h)"),
|
|
50
|
+
("13", "Review Monitor - Estado emails/WA resenas"),
|
|
51
|
+
("14", "WhatsApp Analisis Completo - Estadisticas globales"),
|
|
52
|
+
("15", "Google Analytics - Revisar analiticas web"),
|
|
53
|
+
("16", "Email Review - Revisar bandejas y spam"),
|
|
41
54
|
]),
|
|
42
|
-
("
|
|
43
|
-
("
|
|
44
|
-
("
|
|
55
|
+
("Informes y SEO", [
|
|
56
|
+
("17", "Auditoria Search Console (cada 2 semanas)"),
|
|
57
|
+
("18", "Re-envio sitemaps (cada 30 dias)"),
|
|
58
|
+
("19", "Verificacion SEO metas"),
|
|
59
|
+
("20", "Informe Email Semanal (domingos)"),
|
|
45
60
|
]),
|
|
46
61
|
]
|
|
47
62
|
|
|
@@ -49,7 +64,7 @@ MENU_ITEMS = [
|
|
|
49
64
|
def _get_dashboard_alerts() -> list[dict]:
|
|
50
65
|
"""Run proactive dashboard and return alerts."""
|
|
51
66
|
try:
|
|
52
|
-
script =
|
|
67
|
+
script = Path.home() / "claude" / "scripts" / "nexo-proactive-dashboard.py"
|
|
53
68
|
if not script.exists():
|
|
54
69
|
return []
|
|
55
70
|
result = subprocess.run(
|
|
@@ -93,7 +108,7 @@ def handle_menu() -> str:
|
|
|
93
108
|
|
|
94
109
|
lines = []
|
|
95
110
|
lines.append("╔" + "═" * W + "╗")
|
|
96
|
-
lines.append("║" + "NEXO —
|
|
111
|
+
lines.append("║" + "NEXO — CENTRO DE OPERACIONES".center(W) + "║")
|
|
97
112
|
lines.append("║" + date_str.center(W) + "║")
|
|
98
113
|
lines.append("╠" + "═" * W + "╣")
|
|
99
114
|
|
|
@@ -101,30 +116,30 @@ def handle_menu() -> str:
|
|
|
101
116
|
dashboard_alerts = _get_dashboard_alerts()
|
|
102
117
|
memory_reviews = _get_memory_review_summary()
|
|
103
118
|
due = handle_reminders("due")
|
|
104
|
-
has_alerts = dashboard_alerts or memory_reviews["total"] > 0 or (due and "No reminders" not in due)
|
|
119
|
+
has_alerts = dashboard_alerts or memory_reviews["total"] > 0 or (due and "No pending reminders" not in due)
|
|
105
120
|
|
|
106
121
|
if has_alerts:
|
|
107
122
|
lines.append("║" + " PROACTIVE ALERTS".ljust(W) + "║")
|
|
108
123
|
lines.append("╠" + "═" * W + "╣")
|
|
109
124
|
|
|
110
125
|
if dashboard_alerts:
|
|
111
|
-
for alert in dashboard_alerts[:10]:
|
|
126
|
+
for alert in dashboard_alerts[:10]: # Top 10
|
|
112
127
|
sev = alert.get("severity", "low")
|
|
113
128
|
icon = {"high": "!!!", "medium": " ! ", "low": " . "}.get(sev, " . ")
|
|
114
129
|
text = alert.get("title", "")[:W - 8]
|
|
115
130
|
lines.append("║" + f" {icon} {text}".ljust(W) + "║")
|
|
116
131
|
if len(dashboard_alerts) > 10:
|
|
117
132
|
more = len(dashboard_alerts) - 10
|
|
118
|
-
lines.append("║" + f" ...
|
|
133
|
+
lines.append("║" + f" ... y {more} alertas mas".ljust(W) + "║")
|
|
119
134
|
|
|
120
135
|
if memory_reviews["total"] > 0:
|
|
121
136
|
text = (
|
|
122
|
-
f"
|
|
123
|
-
f"({memory_reviews['decisions']}
|
|
137
|
+
f"MEMORIA: {memory_reviews['total']} revisiones pendientes "
|
|
138
|
+
f"({memory_reviews['decisions']} decisiones, {memory_reviews['learnings']} learnings)"
|
|
124
139
|
)[:W - 4]
|
|
125
140
|
lines.append("║" + f" ! {text}".ljust(W) + "║")
|
|
126
141
|
|
|
127
|
-
if due and "No reminders" not in due:
|
|
142
|
+
if due and "No pending reminders" not in due:
|
|
128
143
|
for reminder_line in due.split("\n"):
|
|
129
144
|
if reminder_line.strip():
|
|
130
145
|
truncated = reminder_line[:W - 2]
|
|
@@ -141,23 +156,26 @@ def handle_menu() -> str:
|
|
|
141
156
|
lines.append("║" + entry.ljust(W) + "║")
|
|
142
157
|
lines.append("╠" + "═" * W + "╣")
|
|
143
158
|
|
|
144
|
-
# Backlog: ideas,
|
|
159
|
+
# Backlog: ideas, proyectos futuros, tareas sin fecha o lejanas
|
|
145
160
|
try:
|
|
146
161
|
conn = get_db()
|
|
147
162
|
cutoff = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d")
|
|
163
|
+
# Reminders sin fecha (backlog/ideas)
|
|
148
164
|
no_date = conn.execute(
|
|
149
165
|
"SELECT id, description, category FROM reminders WHERE status LIKE 'PENDIENTE%' AND (date IS NULL OR date='') ORDER BY category, id"
|
|
150
166
|
).fetchall()
|
|
167
|
+
# Reminders con fecha > 7 días (futuro)
|
|
151
168
|
future = conn.execute(
|
|
152
169
|
"SELECT id, description, date, category FROM reminders WHERE status LIKE 'PENDIENTE%' AND date > ? ORDER BY date",
|
|
153
170
|
(cutoff,)
|
|
154
171
|
).fetchall()
|
|
172
|
+
# Followups sin fecha
|
|
155
173
|
nf_no_date = conn.execute(
|
|
156
174
|
"SELECT id, description FROM followups WHERE status NOT LIKE 'COMPLETADO%' AND (date IS NULL OR date='') ORDER BY id"
|
|
157
175
|
).fetchall()
|
|
158
176
|
|
|
159
177
|
if no_date or future or nf_no_date:
|
|
160
|
-
lines.append("║" + " BACKLOG / IDEAS /
|
|
178
|
+
lines.append("║" + " BACKLOG / IDEAS / FUTURO".ljust(W) + "║")
|
|
161
179
|
lines.append("║" + "─" * W + "║")
|
|
162
180
|
|
|
163
181
|
if no_date:
|
|
@@ -172,29 +190,29 @@ def handle_menu() -> str:
|
|
|
172
190
|
lines.append("║" + f" {r['id']}: {short}".ljust(W) + "║")
|
|
173
191
|
|
|
174
192
|
if future:
|
|
175
|
-
lines.append("║" + f" [
|
|
193
|
+
lines.append("║" + f" [Programado]".ljust(W) + "║")
|
|
176
194
|
for r in future:
|
|
177
195
|
short = r["description"][:W - 18]
|
|
178
196
|
lines.append("║" + f" {r['id']} ({r['date']}): {short}".ljust(W) + "║")
|
|
179
197
|
|
|
180
198
|
if nf_no_date:
|
|
181
|
-
lines.append("║" + f" [
|
|
199
|
+
lines.append("║" + f" [Followups pendientes]".ljust(W) + "║")
|
|
182
200
|
for r in nf_no_date:
|
|
183
201
|
short = r["description"][:W - 12]
|
|
184
202
|
lines.append("║" + f" {r['id']}: {short}".ljust(W) + "║")
|
|
185
203
|
|
|
186
204
|
lines.append("╠" + "═" * W + "╣")
|
|
187
205
|
except Exception as e:
|
|
188
|
-
lines.append("║" + f"
|
|
206
|
+
lines.append("║" + f" ⚠ Error backlog: {e}".ljust(W) + "║")
|
|
189
207
|
lines.append("╠" + "═" * W + "╣")
|
|
190
208
|
|
|
191
209
|
# Active sessions
|
|
192
210
|
sessions = handle_status()
|
|
193
|
-
if "No sessions" not in sessions:
|
|
211
|
+
if "No active sessions" not in sessions:
|
|
194
212
|
lines.append("║" + " ACTIVE SESSIONS".ljust(W) + "║")
|
|
195
213
|
lines.append("║" + "─" * W + "║")
|
|
196
214
|
for s_line in sessions.split("\n"):
|
|
197
|
-
if s_line.strip() and "
|
|
215
|
+
if s_line.strip() and "ACTIVE SESSIONS" not in s_line:
|
|
198
216
|
truncated = s_line[:W - 2]
|
|
199
217
|
lines.append("║" + f" {truncated}".ljust(W) + "║")
|
|
200
218
|
lines.append("╠" + "═" * W + "╣")
|
|
@@ -14,15 +14,15 @@ from db import (
|
|
|
14
14
|
def handle_reminder_create(id: str, description: str, date: str = '', category: str = 'general') -> str:
|
|
15
15
|
"""Create a new reminder. id must start with 'R'."""
|
|
16
16
|
if not id.startswith('R'):
|
|
17
|
-
return f"ERROR:
|
|
17
|
+
return f"ERROR: El ID del recordatorio debe empezar por 'R' (recibido: '{id}')."
|
|
18
18
|
|
|
19
19
|
result = create_reminder(id=id, description=description, date=date or None, category=category)
|
|
20
20
|
if not result or "error" in result:
|
|
21
|
-
error_msg = result.get("error", "
|
|
21
|
+
error_msg = result.get("error", "desconocido") if isinstance(result, dict) else "desconocido"
|
|
22
22
|
return f"ERROR: {error_msg}"
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
return f"Reminder {id} created. Date: {
|
|
24
|
+
fecha_str = date if date else 'no date'
|
|
25
|
+
return f"Reminder {id} created. Date: {fecha_str}. Category: {category}."
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def handle_reminder_update(id: str, description: str = '', date: str = '', status: str = '', category: str = '') -> str:
|
|
@@ -85,12 +85,12 @@ def handle_followup_create(id: str, description: str, date: str = '', verificati
|
|
|
85
85
|
|
|
86
86
|
result = create_followup(id=id, description=description, date=date or None, verification=verification, reasoning=reasoning, recurrence=recurrence or None)
|
|
87
87
|
if not result or "error" in result:
|
|
88
|
-
error_msg = result.get("error", "
|
|
88
|
+
error_msg = result.get("error", "desconocido") if isinstance(result, dict) else "desconocido"
|
|
89
89
|
return f"ERROR: {error_msg}"
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
fecha_str = date if date else 'no date'
|
|
92
92
|
rec_str = f" Recurrence: {recurrence}." if recurrence else ""
|
|
93
|
-
return f"Followup {id} created. Date: {
|
|
93
|
+
return f"Followup {id} created. Date: {fecha_str}.{rec_str}"
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def handle_followup_update(id: str, description: str = '', date: str = '', verification: str = '', status: str = '') -> str:
|
|
@@ -136,10 +136,10 @@ def handle_followup_complete(id: str, result: str = '') -> str:
|
|
|
136
136
|
# The new one was auto-created by complete_followup
|
|
137
137
|
new_row = conn.execute("SELECT date FROM followups WHERE id = ?", (id,)).fetchone()
|
|
138
138
|
if new_row:
|
|
139
|
-
msg += f"
|
|
139
|
+
msg += f" ♻️ Siguiente auto-creado para {new_row['date']}."
|
|
140
140
|
linked_decisions = find_decisions_by_context_ref(id)
|
|
141
141
|
if linked_decisions:
|
|
142
|
-
outcome_text = result if result else f"Followup {id}
|
|
142
|
+
outcome_text = result if result else f"Followup {id} completado"
|
|
143
143
|
for dec in linked_decisions:
|
|
144
144
|
update_decision_outcome(dec['id'], outcome_text)
|
|
145
145
|
dec_ids = ', '.join(f"#{d['id']}" for d in linked_decisions)
|
package/src/tools_sessions.py
CHANGED
|
@@ -79,16 +79,16 @@ def handle_startup(task: str = "Startup") -> str:
|
|
|
79
79
|
|
|
80
80
|
if other_sessions:
|
|
81
81
|
lines.append("")
|
|
82
|
-
lines.append("
|
|
82
|
+
lines.append("ACTIVE SESSIONS:")
|
|
83
83
|
for s in other_sessions:
|
|
84
84
|
age = _format_age(s["last_update_epoch"])
|
|
85
85
|
lines.append(f" {s['sid']} ({age}) — {s['task']}")
|
|
86
86
|
else:
|
|
87
|
-
lines.append("
|
|
87
|
+
lines.append("No other active sessions.")
|
|
88
88
|
|
|
89
89
|
if inbox:
|
|
90
90
|
lines.append("")
|
|
91
|
-
lines.append("
|
|
91
|
+
lines.append("PENDING MESSAGES:")
|
|
92
92
|
for m in inbox:
|
|
93
93
|
age = _format_age(m["created_epoch"])
|
|
94
94
|
lines.append(f" [{m['from_sid']}] ({age}): {m['text']}")
|
|
@@ -112,7 +112,7 @@ def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
|
|
|
112
112
|
inbox = get_inbox(sid)
|
|
113
113
|
if inbox:
|
|
114
114
|
parts.append("")
|
|
115
|
-
parts.append("
|
|
115
|
+
parts.append("MESSAGES:")
|
|
116
116
|
for m in inbox:
|
|
117
117
|
age = _format_age(m["created_epoch"])
|
|
118
118
|
parts.append(f" [{m['from_sid']}] ({age}): {m['text']}")
|
|
@@ -120,7 +120,7 @@ def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
|
|
|
120
120
|
questions = get_pending_questions(sid)
|
|
121
121
|
if questions:
|
|
122
122
|
parts.append("")
|
|
123
|
-
parts.append("
|
|
123
|
+
parts.append("PENDING QUESTIONS (reply with nexo_answer):")
|
|
124
124
|
for q in questions:
|
|
125
125
|
age = _format_age(q["created_epoch"])
|
|
126
126
|
parts.append(f" {q['qid']} de {q['from_sid']} ({age}): {q['question']}")
|
|
@@ -296,7 +296,7 @@ def handle_stop(sid: str) -> str:
|
|
|
296
296
|
"""Cleanly close a session, removing it from active sessions immediately."""
|
|
297
297
|
_stop_keepalive(sid)
|
|
298
298
|
complete_session(sid)
|
|
299
|
-
return f"
|
|
299
|
+
return f"Session {sid} closed."
|
|
300
300
|
|
|
301
301
|
|
|
302
302
|
def handle_status(keyword: str | None = None) -> str:
|
|
@@ -310,9 +310,9 @@ def handle_status(keyword: str | None = None) -> str:
|
|
|
310
310
|
sessions = get_active_sessions()
|
|
311
311
|
|
|
312
312
|
if not sessions:
|
|
313
|
-
return "
|
|
313
|
+
return "No active sessions."
|
|
314
314
|
|
|
315
|
-
lines = ["
|
|
315
|
+
lines = ["ACTIVE SESSIONS:"]
|
|
316
316
|
for s in sessions:
|
|
317
317
|
age = _format_age(s["last_update_epoch"])
|
|
318
318
|
lines.append(f" {s['sid']} ({age}) — {s['task']}")
|
|
@@ -28,9 +28,9 @@ def handle_task_list(task_num: str = '', days: int = 30) -> str:
|
|
|
28
28
|
"""Show execution history for all tasks or a specific task number."""
|
|
29
29
|
results = list_task_history(task_num if task_num else None, days)
|
|
30
30
|
if not results:
|
|
31
|
-
scope = f"task {task_num}" if task_num else "
|
|
32
|
-
return f"HISTORY: No executions
|
|
33
|
-
lines = [f"
|
|
31
|
+
scope = f"task {task_num}" if task_num else "any task"
|
|
32
|
+
return f"HISTORY: No executions for {scope} in the last {days} days."
|
|
33
|
+
lines = [f"HISTORY ({len(results)} executions, {days}d):"]
|
|
34
34
|
for r in results:
|
|
35
35
|
date_str = _epoch_to_date(r["executed_at"])
|
|
36
36
|
notes_str = f": {r['notes']}" if r.get("notes") else ""
|
|
@@ -43,15 +43,15 @@ def handle_task_frequency() -> str:
|
|
|
43
43
|
overdue = get_overdue_tasks()
|
|
44
44
|
if not overdue:
|
|
45
45
|
return "All tasks up to date."
|
|
46
|
-
lines = ["
|
|
46
|
+
lines = ["OVERDUE TASKS:"]
|
|
47
47
|
for t in overdue:
|
|
48
48
|
days_since = t.get("days_since_last")
|
|
49
49
|
if days_since is not None:
|
|
50
|
-
since_str = f"
|
|
50
|
+
since_str = f"last run {days_since:.1f} days ago"
|
|
51
51
|
else:
|
|
52
|
-
since_str = "
|
|
52
|
+
since_str = "never executed"
|
|
53
53
|
lines.append(
|
|
54
54
|
f" Task {t['task_num']} ({t['task_name']}): "
|
|
55
|
-
f"{since_str},
|
|
55
|
+
f"{since_str}, frequency every {t['frequency_days']} days"
|
|
56
56
|
)
|
|
57
57
|
return "\n".join(lines)
|