nexo-brain 2.6.16 → 2.6.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +12 -2
- package/package.json +1 -1
- package/src/auto_update.py +10 -1
- package/src/bootstrap_docs.py +3 -1
- package/src/client_preferences.py +68 -2
- package/src/client_sync.py +45 -21
- package/src/cognitive/_search.py +30 -3
- package/src/doctor/providers/runtime.py +325 -0
- package/src/plugins/cognitive_memory.py +4 -0
- package/src/plugins/update.py +10 -1
- package/src/scripts/deep-sleep/apply_findings.py +393 -0
- package/src/scripts/deep-sleep/collect.py +221 -0
- package/src/scripts/deep-sleep/synthesize-prompt.md +13 -0
- package/src/scripts/deep-sleep/synthesize.py +1 -0
- package/src/scripts/nexo-update.sh +7 -1
|
@@ -19,6 +19,7 @@ import json
|
|
|
19
19
|
import os
|
|
20
20
|
import sqlite3
|
|
21
21
|
import sys
|
|
22
|
+
from collections import Counter
|
|
22
23
|
from datetime import datetime, timedelta
|
|
23
24
|
from pathlib import Path
|
|
24
25
|
|
|
@@ -309,6 +310,392 @@ def create_abandoned_followups(synthesis: dict) -> list[dict]:
|
|
|
309
310
|
return results
|
|
310
311
|
|
|
311
312
|
|
|
313
|
+
def _safe_query(db_path: Path, query: str, params: tuple = ()) -> list[dict]:
|
|
314
|
+
if not db_path.exists():
|
|
315
|
+
return []
|
|
316
|
+
try:
|
|
317
|
+
conn = sqlite3.connect(str(db_path))
|
|
318
|
+
conn.row_factory = sqlite3.Row
|
|
319
|
+
rows = conn.execute(query, params).fetchall()
|
|
320
|
+
conn.close()
|
|
321
|
+
return [dict(row) for row in rows]
|
|
322
|
+
except Exception:
|
|
323
|
+
return []
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _parse_any_datetime(value) -> datetime | None:
|
|
327
|
+
if value in (None, ""):
|
|
328
|
+
return None
|
|
329
|
+
try:
|
|
330
|
+
if isinstance(value, (int, float)) or (isinstance(value, str) and str(value).strip().isdigit()):
|
|
331
|
+
return datetime.fromtimestamp(float(value))
|
|
332
|
+
except Exception:
|
|
333
|
+
return None
|
|
334
|
+
raw = str(value).strip()
|
|
335
|
+
for fmt in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S"):
|
|
336
|
+
try:
|
|
337
|
+
return datetime.strptime(raw[:19], fmt)
|
|
338
|
+
except Exception:
|
|
339
|
+
continue
|
|
340
|
+
try:
|
|
341
|
+
return datetime.fromisoformat(raw.replace("Z", "+00:00").replace("+00:00", ""))
|
|
342
|
+
except Exception:
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _load_project_aliases() -> dict[str, set[str]]:
|
|
347
|
+
atlas_path = NEXO_HOME / "brain" / "project-atlas.json"
|
|
348
|
+
if not atlas_path.is_file():
|
|
349
|
+
return {}
|
|
350
|
+
try:
|
|
351
|
+
payload = json.loads(atlas_path.read_text())
|
|
352
|
+
except Exception:
|
|
353
|
+
return {}
|
|
354
|
+
if not isinstance(payload, dict):
|
|
355
|
+
return {}
|
|
356
|
+
aliases: dict[str, set[str]] = {}
|
|
357
|
+
for key, value in payload.items():
|
|
358
|
+
if str(key).startswith("_"):
|
|
359
|
+
continue
|
|
360
|
+
canonical = str(key).strip().lower()
|
|
361
|
+
alias_set = {canonical, canonical.replace("-", " "), canonical.replace("_", " ")}
|
|
362
|
+
if isinstance(value, dict):
|
|
363
|
+
for alias in value.get("aliases", []) or []:
|
|
364
|
+
alias_value = str(alias or "").strip().lower()
|
|
365
|
+
if alias_value:
|
|
366
|
+
alias_set.add(alias_value)
|
|
367
|
+
alias_set.add(alias_value.replace("-", " "))
|
|
368
|
+
aliases[canonical] = {item for item in alias_set if item}
|
|
369
|
+
return aliases
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _match_projects(text: str, alias_map: dict[str, set[str]]) -> set[str]:
|
|
373
|
+
haystack = str(text or "").strip().lower()
|
|
374
|
+
if not haystack:
|
|
375
|
+
return set()
|
|
376
|
+
matches: set[str] = set()
|
|
377
|
+
for canonical, aliases in alias_map.items():
|
|
378
|
+
for alias in sorted(aliases, key=len, reverse=True):
|
|
379
|
+
if alias and alias in haystack:
|
|
380
|
+
matches.add(canonical)
|
|
381
|
+
break
|
|
382
|
+
return matches
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _priority_weight(value) -> float:
|
|
386
|
+
lowered = str(value or "").strip().lower()
|
|
387
|
+
if lowered in {"critical", "urgent"}:
|
|
388
|
+
return 4.0
|
|
389
|
+
if lowered == "high":
|
|
390
|
+
return 3.0
|
|
391
|
+
if lowered == "medium":
|
|
392
|
+
return 2.0
|
|
393
|
+
if lowered == "low":
|
|
394
|
+
return 1.0
|
|
395
|
+
return 1.5
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _project_weighting_window(target_date: str, *, window_days: int) -> list[dict]:
|
|
399
|
+
target_day = datetime.strptime(target_date, "%Y-%m-%d")
|
|
400
|
+
window_start = target_day - timedelta(days=max(0, window_days - 1))
|
|
401
|
+
alias_map = _load_project_aliases()
|
|
402
|
+
scoreboard: dict[str, dict] = {}
|
|
403
|
+
|
|
404
|
+
def normalize_project(project: str) -> str:
|
|
405
|
+
lowered = str(project or "").strip().lower()
|
|
406
|
+
if not lowered:
|
|
407
|
+
return ""
|
|
408
|
+
matched = _match_projects(lowered, alias_map)
|
|
409
|
+
if matched:
|
|
410
|
+
return sorted(matched)[0]
|
|
411
|
+
return lowered
|
|
412
|
+
|
|
413
|
+
def bump(project: str, score: float, signal_key: str, reason: str) -> None:
|
|
414
|
+
canonical = normalize_project(project)
|
|
415
|
+
if not canonical:
|
|
416
|
+
return
|
|
417
|
+
slot = scoreboard.setdefault(
|
|
418
|
+
canonical,
|
|
419
|
+
{
|
|
420
|
+
"project": canonical,
|
|
421
|
+
"score": 0.0,
|
|
422
|
+
"signals": {
|
|
423
|
+
"diary_sessions": 0,
|
|
424
|
+
"learnings": 0,
|
|
425
|
+
"followups": 0,
|
|
426
|
+
"decisions": 0,
|
|
427
|
+
},
|
|
428
|
+
"reasons": [],
|
|
429
|
+
},
|
|
430
|
+
)
|
|
431
|
+
slot["score"] += score
|
|
432
|
+
slot["signals"][signal_key] += 1
|
|
433
|
+
if reason and reason not in slot["reasons"]:
|
|
434
|
+
slot["reasons"].append(reason)
|
|
435
|
+
|
|
436
|
+
diary_rows = _safe_query(
|
|
437
|
+
NEXO_DB,
|
|
438
|
+
"SELECT created_at, summary, self_critique, domain FROM session_diary ORDER BY created_at DESC",
|
|
439
|
+
)
|
|
440
|
+
for row in diary_rows:
|
|
441
|
+
created = _parse_any_datetime(row.get("created_at"))
|
|
442
|
+
if not created or created < window_start or created > target_day + timedelta(days=1):
|
|
443
|
+
continue
|
|
444
|
+
recency_bonus = 1.4 if (target_day - created).days <= 7 else 1.0
|
|
445
|
+
matched = _match_projects(
|
|
446
|
+
" ".join(
|
|
447
|
+
[
|
|
448
|
+
str(row.get("summary", "") or ""),
|
|
449
|
+
str(row.get("self_critique", "") or ""),
|
|
450
|
+
]
|
|
451
|
+
),
|
|
452
|
+
alias_map,
|
|
453
|
+
)
|
|
454
|
+
domain = normalize_project(str(row.get("domain", "") or ""))
|
|
455
|
+
if domain:
|
|
456
|
+
matched.add(domain)
|
|
457
|
+
for project in matched:
|
|
458
|
+
bump(project, 3.0 * recency_bonus, "diary_sessions", "recent diary activity")
|
|
459
|
+
|
|
460
|
+
learning_rows = _safe_query(
|
|
461
|
+
NEXO_DB,
|
|
462
|
+
"SELECT title, content, applies_to, priority, weight, updated_at, created_at FROM learnings "
|
|
463
|
+
"ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 180",
|
|
464
|
+
)
|
|
465
|
+
for row in learning_rows:
|
|
466
|
+
when = _parse_any_datetime(row.get("updated_at") or row.get("created_at"))
|
|
467
|
+
if when and when < window_start:
|
|
468
|
+
continue
|
|
469
|
+
matched = _match_projects(
|
|
470
|
+
" ".join(
|
|
471
|
+
[
|
|
472
|
+
str(row.get("applies_to", "") or ""),
|
|
473
|
+
str(row.get("title", "") or ""),
|
|
474
|
+
str(row.get("content", "") or ""),
|
|
475
|
+
]
|
|
476
|
+
),
|
|
477
|
+
alias_map,
|
|
478
|
+
)
|
|
479
|
+
if not matched:
|
|
480
|
+
continue
|
|
481
|
+
score = 1.0 + _priority_weight(row.get("priority")) + min(2.0, max(0.0, float(row.get("weight", 0) or 0)))
|
|
482
|
+
for project in matched:
|
|
483
|
+
bump(project, score, "learnings", "recent leverage-bearing learning")
|
|
484
|
+
|
|
485
|
+
followup_rows = _safe_query(
|
|
486
|
+
NEXO_DB,
|
|
487
|
+
"SELECT description, date, status, priority, created_at, updated_at, reasoning FROM followups "
|
|
488
|
+
"WHERE status NOT IN ('COMPLETED', 'CANCELLED') ORDER BY date ASC, created_at ASC LIMIT 160",
|
|
489
|
+
)
|
|
490
|
+
for row in followup_rows:
|
|
491
|
+
matched = _match_projects(
|
|
492
|
+
" ".join(
|
|
493
|
+
[
|
|
494
|
+
str(row.get("description", "") or ""),
|
|
495
|
+
str(row.get("reasoning", "") or ""),
|
|
496
|
+
]
|
|
497
|
+
),
|
|
498
|
+
alias_map,
|
|
499
|
+
)
|
|
500
|
+
if not matched:
|
|
501
|
+
continue
|
|
502
|
+
overdue_bonus = 0.0
|
|
503
|
+
due_dt = _parse_any_datetime(row.get("date"))
|
|
504
|
+
if due_dt and due_dt <= target_day:
|
|
505
|
+
overdue_bonus = 1.5
|
|
506
|
+
score = 1.5 + _priority_weight(row.get("priority")) + overdue_bonus
|
|
507
|
+
for project in matched:
|
|
508
|
+
bump(project, score, "followups", "open followup pressure")
|
|
509
|
+
|
|
510
|
+
decision_rows = _safe_query(
|
|
511
|
+
NEXO_DB,
|
|
512
|
+
"SELECT domain, outcome, status, reasoning, created_at, review_due_at FROM decisions "
|
|
513
|
+
"ORDER BY COALESCE(created_at, review_due_at) DESC LIMIT 160",
|
|
514
|
+
)
|
|
515
|
+
for row in decision_rows:
|
|
516
|
+
when = _parse_any_datetime(row.get("created_at") or row.get("review_due_at"))
|
|
517
|
+
if when and when < window_start:
|
|
518
|
+
continue
|
|
519
|
+
matched = _match_projects(
|
|
520
|
+
" ".join(
|
|
521
|
+
[
|
|
522
|
+
str(row.get("reasoning", "") or ""),
|
|
523
|
+
str(row.get("outcome", "") or ""),
|
|
524
|
+
str(row.get("status", "") or ""),
|
|
525
|
+
]
|
|
526
|
+
),
|
|
527
|
+
alias_map,
|
|
528
|
+
)
|
|
529
|
+
domain = normalize_project(str(row.get("domain", "") or ""))
|
|
530
|
+
if domain:
|
|
531
|
+
matched.add(domain)
|
|
532
|
+
if not matched:
|
|
533
|
+
continue
|
|
534
|
+
outcome = str(row.get("outcome", "") or "").lower()
|
|
535
|
+
status = str(row.get("status", "") or "").lower()
|
|
536
|
+
score = 2.5
|
|
537
|
+
if any(token in outcome for token in ("fail", "error", "blocked", "regression")):
|
|
538
|
+
score += 2.0
|
|
539
|
+
if status in {"pending", "blocked", "open"}:
|
|
540
|
+
score += 1.5
|
|
541
|
+
for project in matched:
|
|
542
|
+
bump(project, score, "decisions", "recent decision pressure")
|
|
543
|
+
|
|
544
|
+
ranked = sorted(scoreboard.values(), key=lambda item: item["score"], reverse=True)
|
|
545
|
+
for item in ranked:
|
|
546
|
+
item["score"] = round(item["score"], 2)
|
|
547
|
+
item["reasons"] = item["reasons"][:4]
|
|
548
|
+
return ranked[:8]
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _load_period_syntheses(target_date: str, *, window_days: int) -> list[dict]:
|
|
552
|
+
target_day = datetime.strptime(target_date, "%Y-%m-%d")
|
|
553
|
+
syntheses: list[dict] = []
|
|
554
|
+
for offset in range(window_days):
|
|
555
|
+
date_str = (target_day - timedelta(days=offset)).strftime("%Y-%m-%d")
|
|
556
|
+
path = DEEP_SLEEP_DIR / f"{date_str}-synthesis.json"
|
|
557
|
+
if not path.is_file():
|
|
558
|
+
continue
|
|
559
|
+
try:
|
|
560
|
+
payload = json.loads(path.read_text())
|
|
561
|
+
except Exception:
|
|
562
|
+
continue
|
|
563
|
+
if isinstance(payload, dict):
|
|
564
|
+
syntheses.append(payload)
|
|
565
|
+
syntheses.reverse()
|
|
566
|
+
return syntheses
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _build_period_summary(target_date: str, synthesis: dict, *, kind: str, window_days: int) -> dict:
|
|
570
|
+
target_day = datetime.strptime(target_date, "%Y-%m-%d")
|
|
571
|
+
window_start = (target_day - timedelta(days=max(0, window_days - 1))).strftime("%Y-%m-%d")
|
|
572
|
+
label = (
|
|
573
|
+
f"{target_day.isocalendar().year}-W{target_day.isocalendar().week:02d}"
|
|
574
|
+
if kind == "weekly"
|
|
575
|
+
else target_day.strftime("%Y-%m")
|
|
576
|
+
)
|
|
577
|
+
syntheses = _load_period_syntheses(target_date, window_days=window_days)
|
|
578
|
+
if not any(item.get("date") == target_date for item in syntheses):
|
|
579
|
+
syntheses.append(synthesis)
|
|
580
|
+
|
|
581
|
+
mood_scores = []
|
|
582
|
+
trust_scores = []
|
|
583
|
+
total_corrections = 0
|
|
584
|
+
pattern_counter: Counter[str] = Counter()
|
|
585
|
+
agenda_counter: Counter[str] = Counter()
|
|
586
|
+
for item in syntheses:
|
|
587
|
+
mood = item.get("emotional_day", {}).get("mood_score")
|
|
588
|
+
if isinstance(mood, (int, float)):
|
|
589
|
+
mood_scores.append(float(mood))
|
|
590
|
+
trust = item.get("trust_calibration", {}).get("score")
|
|
591
|
+
if isinstance(trust, (int, float)):
|
|
592
|
+
trust_scores.append(float(trust))
|
|
593
|
+
total_corrections += int(item.get("productivity_day", {}).get("total_corrections", 0) or 0)
|
|
594
|
+
for pattern in item.get("cross_session_patterns", []) or []:
|
|
595
|
+
text = str(pattern.get("pattern", "") or "").strip()
|
|
596
|
+
if text:
|
|
597
|
+
pattern_counter[text] += 1
|
|
598
|
+
for agenda in item.get("morning_agenda", []) or []:
|
|
599
|
+
title = str(agenda.get("title", "") or "").strip()
|
|
600
|
+
if title:
|
|
601
|
+
agenda_counter[title] += 1
|
|
602
|
+
|
|
603
|
+
top_projects = _project_weighting_window(target_date, window_days=window_days)
|
|
604
|
+
avg_mood = round(sum(mood_scores) / len(mood_scores), 3) if mood_scores else None
|
|
605
|
+
avg_trust = round(sum(trust_scores) / len(trust_scores), 1) if trust_scores else None
|
|
606
|
+
top_patterns = [
|
|
607
|
+
{"pattern": pattern, "count": count}
|
|
608
|
+
for pattern, count in pattern_counter.most_common(6)
|
|
609
|
+
]
|
|
610
|
+
recurring_agenda = [
|
|
611
|
+
{"title": title, "count": count}
|
|
612
|
+
for title, count in agenda_counter.most_common(6)
|
|
613
|
+
]
|
|
614
|
+
|
|
615
|
+
summary_parts = [f"{len(syntheses)} Deep Sleep run(s)"]
|
|
616
|
+
if top_projects:
|
|
617
|
+
summary_parts.append(f"top focus: {top_projects[0]['project']}")
|
|
618
|
+
if top_patterns:
|
|
619
|
+
summary_parts.append(f"recurring pattern: {top_patterns[0]['pattern']}")
|
|
620
|
+
if avg_trust is not None:
|
|
621
|
+
summary_parts.append(f"avg trust {avg_trust:.1f}")
|
|
622
|
+
summary = " | ".join(summary_parts)
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
"kind": kind,
|
|
626
|
+
"label": label,
|
|
627
|
+
"window_days": window_days,
|
|
628
|
+
"window_start": window_start,
|
|
629
|
+
"window_end": target_date,
|
|
630
|
+
"generated_at": datetime.now().isoformat(),
|
|
631
|
+
"daily_syntheses": len(syntheses),
|
|
632
|
+
"avg_mood_score": avg_mood,
|
|
633
|
+
"avg_trust_score": avg_trust,
|
|
634
|
+
"total_corrections": total_corrections,
|
|
635
|
+
"top_projects": top_projects,
|
|
636
|
+
"top_patterns": top_patterns,
|
|
637
|
+
"recurring_agenda": recurring_agenda,
|
|
638
|
+
"summary": summary,
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _render_period_summary_markdown(summary: dict) -> str:
|
|
643
|
+
lines = [
|
|
644
|
+
f"# {summary.get('kind', 'period').title()} Deep Sleep Summary — {summary.get('label', '')}",
|
|
645
|
+
"",
|
|
646
|
+
f"- Window: {summary.get('window_start', '')} -> {summary.get('window_end', '')}",
|
|
647
|
+
f"- Deep Sleep runs: {summary.get('daily_syntheses', 0)}",
|
|
648
|
+
]
|
|
649
|
+
if summary.get("avg_mood_score") is not None:
|
|
650
|
+
lines.append(f"- Avg mood score: {summary['avg_mood_score']:.2f}")
|
|
651
|
+
if summary.get("avg_trust_score") is not None:
|
|
652
|
+
lines.append(f"- Avg trust score: {summary['avg_trust_score']:.1f}")
|
|
653
|
+
lines.append(f"- Total corrections: {summary.get('total_corrections', 0)}")
|
|
654
|
+
lines.append("")
|
|
655
|
+
if summary.get("summary"):
|
|
656
|
+
lines.append(f"> {summary['summary']}")
|
|
657
|
+
lines.append("")
|
|
658
|
+
|
|
659
|
+
if summary.get("top_projects"):
|
|
660
|
+
lines.append("## Top Projects")
|
|
661
|
+
lines.append("")
|
|
662
|
+
for item in summary["top_projects"][:5]:
|
|
663
|
+
lines.append(f"- **{item['project']}** — score {item['score']}")
|
|
664
|
+
if item.get("reasons"):
|
|
665
|
+
lines.append(f" Reasons: {', '.join(item['reasons'])}")
|
|
666
|
+
lines.append("")
|
|
667
|
+
|
|
668
|
+
if summary.get("top_patterns"):
|
|
669
|
+
lines.append("## Recurring Patterns")
|
|
670
|
+
lines.append("")
|
|
671
|
+
for item in summary["top_patterns"][:5]:
|
|
672
|
+
lines.append(f"- {item['pattern']} ({item['count']}x)")
|
|
673
|
+
lines.append("")
|
|
674
|
+
|
|
675
|
+
if summary.get("recurring_agenda"):
|
|
676
|
+
lines.append("## Recurring Agenda")
|
|
677
|
+
lines.append("")
|
|
678
|
+
for item in summary["recurring_agenda"][:5]:
|
|
679
|
+
lines.append(f"- {item['title']} ({item['count']}x)")
|
|
680
|
+
lines.append("")
|
|
681
|
+
|
|
682
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def write_periodic_summaries(target_date: str, synthesis: dict) -> dict:
|
|
686
|
+
outputs: dict[str, str] = {}
|
|
687
|
+
for kind, window_days in (("weekly", 7), ("monthly", 30)):
|
|
688
|
+
summary = _build_period_summary(target_date, synthesis, kind=kind, window_days=window_days)
|
|
689
|
+
label = summary["label"]
|
|
690
|
+
json_path = DEEP_SLEEP_DIR / f"{label}-{kind}-summary.json"
|
|
691
|
+
md_path = DEEP_SLEEP_DIR / f"{label}-{kind}-summary.md"
|
|
692
|
+
json_path.write_text(json.dumps(summary, indent=2, ensure_ascii=False))
|
|
693
|
+
md_path.write_text(_render_period_summary_markdown(summary), encoding="utf-8")
|
|
694
|
+
outputs[f"{kind}_json"] = str(json_path)
|
|
695
|
+
outputs[f"{kind}_markdown"] = str(md_path)
|
|
696
|
+
return outputs
|
|
697
|
+
|
|
698
|
+
|
|
312
699
|
def generate_session_tone(synthesis: dict, target_date: str) -> dict:
|
|
313
700
|
"""Generate emotional tone guidance for next session startup.
|
|
314
701
|
|
|
@@ -730,6 +1117,11 @@ def main():
|
|
|
730
1117
|
briefing_path = write_morning_briefing(target_date, synthesis)
|
|
731
1118
|
print(f" Briefing: {briefing_path}")
|
|
732
1119
|
|
|
1120
|
+
print("[apply] Writing weekly/monthly Deep Sleep summaries...")
|
|
1121
|
+
periodic_outputs = write_periodic_summaries(target_date, synthesis)
|
|
1122
|
+
for label, path in periodic_outputs.items():
|
|
1123
|
+
print(f" {label}: {path}")
|
|
1124
|
+
|
|
733
1125
|
# Write applied log
|
|
734
1126
|
applied_log = {
|
|
735
1127
|
"date": target_date,
|
|
@@ -738,6 +1130,7 @@ def main():
|
|
|
738
1130
|
"stats": stats,
|
|
739
1131
|
"applied_actions": applied_actions,
|
|
740
1132
|
"summary": synthesis.get("summary", ""),
|
|
1133
|
+
"periodic_summaries": periodic_outputs,
|
|
741
1134
|
}
|
|
742
1135
|
|
|
743
1136
|
applied_file = DEEP_SLEEP_DIR / f"{target_date}-applied.json"
|
|
@@ -390,6 +390,220 @@ def _compact_diary_row(row: dict) -> dict:
|
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
|
|
393
|
+
def _load_project_aliases() -> dict[str, set[str]]:
|
|
394
|
+
atlas_path = NEXO_HOME / "brain" / "project-atlas.json"
|
|
395
|
+
aliases: dict[str, set[str]] = {}
|
|
396
|
+
if not atlas_path.is_file():
|
|
397
|
+
return aliases
|
|
398
|
+
try:
|
|
399
|
+
payload = json.loads(atlas_path.read_text())
|
|
400
|
+
except Exception:
|
|
401
|
+
return aliases
|
|
402
|
+
if not isinstance(payload, dict):
|
|
403
|
+
return aliases
|
|
404
|
+
for key, value in payload.items():
|
|
405
|
+
if str(key).startswith("_"):
|
|
406
|
+
continue
|
|
407
|
+
canonical = str(key).strip().lower()
|
|
408
|
+
alias_set = {canonical, canonical.replace("-", " "), canonical.replace("_", " ")}
|
|
409
|
+
if isinstance(value, dict):
|
|
410
|
+
for alias in value.get("aliases", []) or []:
|
|
411
|
+
alias_value = str(alias or "").strip().lower()
|
|
412
|
+
if alias_value:
|
|
413
|
+
alias_set.add(alias_value)
|
|
414
|
+
alias_set.add(alias_value.replace("-", " "))
|
|
415
|
+
aliases[canonical] = {item for item in alias_set if item}
|
|
416
|
+
return aliases
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _match_projects(text: str, alias_map: dict[str, set[str]]) -> set[str]:
|
|
420
|
+
haystack = str(text or "").strip().lower()
|
|
421
|
+
if not haystack:
|
|
422
|
+
return set()
|
|
423
|
+
matches: set[str] = set()
|
|
424
|
+
for canonical, aliases in alias_map.items():
|
|
425
|
+
for alias in sorted(aliases, key=len, reverse=True):
|
|
426
|
+
if alias and alias in haystack:
|
|
427
|
+
matches.add(canonical)
|
|
428
|
+
break
|
|
429
|
+
return matches
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _priority_weight(value) -> float:
|
|
433
|
+
lowered = str(value or "").strip().lower()
|
|
434
|
+
if lowered in {"critical", "urgent"}:
|
|
435
|
+
return 4.0
|
|
436
|
+
if lowered == "high":
|
|
437
|
+
return 3.0
|
|
438
|
+
if lowered == "medium":
|
|
439
|
+
return 2.0
|
|
440
|
+
if lowered == "low":
|
|
441
|
+
return 1.0
|
|
442
|
+
return 1.5
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _compact_periodic_summary(data: dict) -> dict:
|
|
446
|
+
return {
|
|
447
|
+
"label": data.get("label", ""),
|
|
448
|
+
"window_start": data.get("window_start", ""),
|
|
449
|
+
"window_end": data.get("window_end", ""),
|
|
450
|
+
"summary": str(data.get("summary", "") or "")[:320],
|
|
451
|
+
"top_projects": data.get("top_projects", [])[:4],
|
|
452
|
+
"top_patterns": data.get("top_patterns", [])[:4],
|
|
453
|
+
"avg_mood_score": data.get("avg_mood_score"),
|
|
454
|
+
"avg_trust_score": data.get("avg_trust_score"),
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _load_periodic_summaries(target_date: str, *, kind: str, limit: int = 2) -> list[dict]:
|
|
459
|
+
target_day = datetime.strptime(target_date, "%Y-%m-%d")
|
|
460
|
+
summaries: list[tuple[str, dict]] = []
|
|
461
|
+
pattern = "*-weekly-summary.json" if kind == "weekly" else "*-monthly-summary.json"
|
|
462
|
+
for path in sorted(DEEP_SLEEP_DIR.glob(pattern)):
|
|
463
|
+
try:
|
|
464
|
+
payload = json.loads(path.read_text())
|
|
465
|
+
except Exception:
|
|
466
|
+
continue
|
|
467
|
+
window_end_raw = str(payload.get("window_end", "") or "")
|
|
468
|
+
parsed = _parse_diary_created_at(window_end_raw)
|
|
469
|
+
if parsed and parsed >= target_day:
|
|
470
|
+
continue
|
|
471
|
+
summaries.append((window_end_raw, _compact_periodic_summary(payload)))
|
|
472
|
+
summaries.sort(key=lambda item: item[0], reverse=True)
|
|
473
|
+
return [item for _, item in summaries[:limit]]
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _project_priority_signals(target_day: datetime, compact_diaries: list[dict]) -> list[dict]:
|
|
477
|
+
alias_map = _load_project_aliases()
|
|
478
|
+
scoreboard: dict[str, dict] = {}
|
|
479
|
+
|
|
480
|
+
def bump(project: str, score: float, signal_key: str, reason: str) -> None:
|
|
481
|
+
if not project:
|
|
482
|
+
return
|
|
483
|
+
slot = scoreboard.setdefault(
|
|
484
|
+
project,
|
|
485
|
+
{
|
|
486
|
+
"project": project,
|
|
487
|
+
"score": 0.0,
|
|
488
|
+
"signals": {
|
|
489
|
+
"diary_sessions": 0,
|
|
490
|
+
"learnings": 0,
|
|
491
|
+
"followups": 0,
|
|
492
|
+
"decisions": 0,
|
|
493
|
+
},
|
|
494
|
+
"reasons": [],
|
|
495
|
+
},
|
|
496
|
+
)
|
|
497
|
+
slot["score"] += score
|
|
498
|
+
slot["signals"][signal_key] += 1
|
|
499
|
+
if reason and reason not in slot["reasons"]:
|
|
500
|
+
slot["reasons"].append(reason)
|
|
501
|
+
|
|
502
|
+
for row in compact_diaries:
|
|
503
|
+
created = _parse_diary_created_at(row.get("created_at"))
|
|
504
|
+
recency_bonus = 1.0
|
|
505
|
+
if created:
|
|
506
|
+
age_days = max(0.0, (target_day - created).total_seconds() / 86400)
|
|
507
|
+
recency_bonus = 1.4 if age_days <= 7 else 1.0
|
|
508
|
+
candidates = set()
|
|
509
|
+
domain = str(row.get("domain", "") or "").strip().lower()
|
|
510
|
+
if domain:
|
|
511
|
+
candidates.add(domain)
|
|
512
|
+
candidates |= _match_projects(" ".join([row.get("summary", ""), row.get("self_critique", "")]), alias_map)
|
|
513
|
+
for project in candidates:
|
|
514
|
+
bump(project, 3.0 * recency_bonus, "diary_sessions", "recent session diary activity")
|
|
515
|
+
|
|
516
|
+
learning_rows = safe_query(
|
|
517
|
+
NEXO_DB,
|
|
518
|
+
"SELECT category, title, content, created_at, updated_at, priority, weight, applies_to FROM learnings "
|
|
519
|
+
"ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 160",
|
|
520
|
+
)
|
|
521
|
+
for row in learning_rows:
|
|
522
|
+
text = " ".join(
|
|
523
|
+
[
|
|
524
|
+
str(row.get("applies_to", "") or ""),
|
|
525
|
+
str(row.get("title", "") or ""),
|
|
526
|
+
str(row.get("content", "") or ""),
|
|
527
|
+
str(row.get("category", "") or ""),
|
|
528
|
+
]
|
|
529
|
+
)
|
|
530
|
+
matched = _match_projects(text, alias_map)
|
|
531
|
+
if not matched:
|
|
532
|
+
continue
|
|
533
|
+
weight = float(row.get("weight", 0) or 0)
|
|
534
|
+
score = 1.0 + _priority_weight(row.get("priority")) + min(2.0, max(0.0, weight))
|
|
535
|
+
for project in matched:
|
|
536
|
+
bump(project, score, "learnings", "recent leverage-bearing learning")
|
|
537
|
+
|
|
538
|
+
followup_rows = safe_query(
|
|
539
|
+
NEXO_DB,
|
|
540
|
+
"SELECT id, description, date, status, priority, created_at, updated_at, reasoning FROM followups "
|
|
541
|
+
"WHERE status NOT IN ('COMPLETED', 'CANCELLED') ORDER BY date ASC, created_at ASC LIMIT 120",
|
|
542
|
+
)
|
|
543
|
+
for row in followup_rows:
|
|
544
|
+
matched = _match_projects(
|
|
545
|
+
" ".join(
|
|
546
|
+
[
|
|
547
|
+
str(row.get("description", "") or ""),
|
|
548
|
+
str(row.get("reasoning", "") or ""),
|
|
549
|
+
]
|
|
550
|
+
),
|
|
551
|
+
alias_map,
|
|
552
|
+
)
|
|
553
|
+
if not matched:
|
|
554
|
+
continue
|
|
555
|
+
overdue_bonus = 0.0
|
|
556
|
+
due_value = str(row.get("date", "") or "")
|
|
557
|
+
try:
|
|
558
|
+
if due_value:
|
|
559
|
+
due_dt = datetime.strptime(due_value[:10], "%Y-%m-%d")
|
|
560
|
+
if due_dt <= target_day:
|
|
561
|
+
overdue_bonus = 1.5
|
|
562
|
+
except Exception:
|
|
563
|
+
overdue_bonus = 0.0
|
|
564
|
+
score = 1.5 + _priority_weight(row.get("priority")) + overdue_bonus
|
|
565
|
+
for project in matched:
|
|
566
|
+
bump(project, score, "followups", "open followup pressure")
|
|
567
|
+
|
|
568
|
+
decision_rows = safe_query(
|
|
569
|
+
NEXO_DB,
|
|
570
|
+
"SELECT domain, outcome, status, reasoning, created_at, review_due_at FROM decisions "
|
|
571
|
+
"ORDER BY COALESCE(created_at, review_due_at) DESC LIMIT 120",
|
|
572
|
+
)
|
|
573
|
+
for row in decision_rows:
|
|
574
|
+
matched = set()
|
|
575
|
+
domain = str(row.get("domain", "") or "").strip().lower()
|
|
576
|
+
if domain:
|
|
577
|
+
matched.add(domain)
|
|
578
|
+
matched |= _match_projects(
|
|
579
|
+
" ".join(
|
|
580
|
+
[
|
|
581
|
+
str(row.get("reasoning", "") or ""),
|
|
582
|
+
str(row.get("outcome", "") or ""),
|
|
583
|
+
str(row.get("status", "") or ""),
|
|
584
|
+
]
|
|
585
|
+
),
|
|
586
|
+
alias_map,
|
|
587
|
+
)
|
|
588
|
+
if not matched:
|
|
589
|
+
continue
|
|
590
|
+
outcome = str(row.get("outcome", "") or "").lower()
|
|
591
|
+
status = str(row.get("status", "") or "").lower()
|
|
592
|
+
score = 2.5
|
|
593
|
+
if any(token in outcome for token in ("fail", "error", "blocked", "regression")):
|
|
594
|
+
score += 2.0
|
|
595
|
+
if status in {"pending", "blocked", "open"}:
|
|
596
|
+
score += 1.5
|
|
597
|
+
for project in matched:
|
|
598
|
+
bump(project, score, "decisions", "recent decision pressure")
|
|
599
|
+
|
|
600
|
+
ranked = sorted(scoreboard.values(), key=lambda item: item["score"], reverse=True)
|
|
601
|
+
for item in ranked:
|
|
602
|
+
item["score"] = round(item["score"], 2)
|
|
603
|
+
item["reasons"] = item["reasons"][:4]
|
|
604
|
+
return ranked[:8]
|
|
605
|
+
|
|
606
|
+
|
|
393
607
|
def collect_long_horizon_context(
|
|
394
608
|
target_date: str,
|
|
395
609
|
*,
|
|
@@ -497,6 +711,10 @@ def collect_long_horizon_context(
|
|
|
497
711
|
"created_at": created.isoformat(),
|
|
498
712
|
})
|
|
499
713
|
|
|
714
|
+
weekly_summaries = _load_periodic_summaries(target_date, kind="weekly", limit=2)
|
|
715
|
+
monthly_summaries = _load_periodic_summaries(target_date, kind="monthly", limit=2)
|
|
716
|
+
project_priority_signals = _project_priority_signals(target_day, compact_diaries)
|
|
717
|
+
|
|
500
718
|
return {
|
|
501
719
|
"horizon_days": horizon_days,
|
|
502
720
|
"recent_window_days": recent_days,
|
|
@@ -508,6 +726,9 @@ def collect_long_horizon_context(
|
|
|
508
726
|
"recurring_mental_states": recurring_states.most_common(8),
|
|
509
727
|
"recurring_self_critiques": recurring_critiques.most_common(6),
|
|
510
728
|
"stale_followups": older_than_week[:12],
|
|
729
|
+
"project_priority_signals": project_priority_signals,
|
|
730
|
+
"weekly_summaries": weekly_summaries,
|
|
731
|
+
"monthly_summaries": monthly_summaries,
|
|
511
732
|
}
|
|
512
733
|
|
|
513
734
|
|
|
@@ -13,6 +13,12 @@ Read the extractions file provided below. It contains per-session findings inclu
|
|
|
13
13
|
Also read the runtime skill candidate file at `{{SKILL_RUNTIME_FILE}}`. It contains mature guide skills with repeated successful usage and candidates for automatic text→script evolution.
|
|
14
14
|
|
|
15
15
|
Also read the long-horizon file at `{{LONG_HORIZON_FILE}}`. It blends recent and older evidence from the last 60 days using a 70% recent / 30% older sample strategy. Use it to detect patterns that a single-day view would miss.
|
|
16
|
+
That long-horizon file may also contain:
|
|
17
|
+
- weekly summaries
|
|
18
|
+
- monthly summaries
|
|
19
|
+
- project priority signals based on diary activity, followup pressure, learnings, and decision outcomes
|
|
20
|
+
|
|
21
|
+
Use those signals to weight importance, leverage, and chronic risk instead of treating all projects equally.
|
|
16
22
|
|
|
17
23
|
Synthesize across all sessions:
|
|
18
24
|
|
|
@@ -23,6 +29,7 @@ Synthesize across all sessions:
|
|
|
23
29
|
- Themes that recur across multiple weeks, not just today
|
|
24
30
|
- Cross-domain connections where an older learning or session sample explains a current issue
|
|
25
31
|
- Topics repeatedly mentioned over time but never formalized into a learning or followup
|
|
32
|
+
- Project pressure that is rising because of repeated diary mentions, open followups, or adverse outcomes
|
|
26
33
|
|
|
27
34
|
### 2. Morning Agenda
|
|
28
35
|
Generate a prioritized agenda for the next morning:
|
|
@@ -59,6 +66,12 @@ Consolidate `abandoned_projects` from all sessions:
|
|
|
59
66
|
- Cross-reference across sessions — was the abandoned work picked up later in another session?
|
|
60
67
|
- Only flag projects that are truly abandoned (no followup AND not resumed)
|
|
61
68
|
|
|
69
|
+
### 6.5 Weekly / Monthly Horizon
|
|
70
|
+
When the long-horizon payload includes weekly or monthly summaries:
|
|
71
|
+
- use them to detect drift across horizons, not just within a single day
|
|
72
|
+
- identify which priorities are rising, stable, or cooling down
|
|
73
|
+
- prefer high-leverage projects when multiple agenda items compete for attention
|
|
74
|
+
|
|
62
75
|
### 7. Trust Calibration (CRITICAL)
|
|
63
76
|
Score the agent's performance for the day on a scale of 0-100. This score becomes the agent's trust score and directly affects its autonomy level the next day. Be fair but honest.
|
|
64
77
|
|