nexo-brain 7.30.0 → 7.30.2
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 +5 -1
- package/package.json +1 -1
- package/src/automation_preferences.py +7 -3
- package/src/cognitive/_core.py +3 -0
- package/src/scripts/deep-sleep/apply_findings.py +75 -6
- package/src/scripts/deep-sleep/synthesize.py +60 -0
- package/src/scripts/nexo-deep-sleep.sh +14 -5
- package/src/scripts/nexo-sleep.py +204 -16
- package/src/tools_sessions.py +202 -0
- package/templates/core-prompts/morning-agent.md +7 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.2",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `7.30.
|
|
21
|
+
Version `7.30.2` is the current packaged-runtime line. Patch release over v7.30.1 - Deep Sleep now fails closed on partial nightly runs, carries complete learning context into synthesis, writes an explicit agent start packet, and creates governed followups with verification, priority, owner, and date fields.
|
|
22
|
+
|
|
23
|
+
Previously in `7.30.1`: patch release over v7.30.0 - morning briefings now behave more like a start-of-day assistant, explain news/weather as verified optional sources, and keep user type inference inside NEXO instead of asking the user to choose a role manually.
|
|
24
|
+
|
|
25
|
+
Previously in `7.30.0`: minor release over v7.29.0 - morning briefing preferences now support weather/news, selectable weekdays, safer SMTP certificates, Opus 4.8 defaults, and an agent-facing preference catalog so NEXO can explain or change supported settings by chat.
|
|
22
26
|
|
|
23
27
|
Previously in `7.29.0`: minor release over v7.28.0 - the morning briefing now has structured content preferences, Desktop-facing briefing presentation state, and per-account email signature support.
|
|
24
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.2",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
|
@@ -83,14 +83,14 @@ MORNING_AGENT_SCHEMA: dict[str, Any] = {
|
|
|
83
83
|
"type": "boolean",
|
|
84
84
|
"label": "News",
|
|
85
85
|
"default": False,
|
|
86
|
-
"help": "A short set of public headlines from the configured news feed
|
|
86
|
+
"help": "A short set of current public headlines from the configured news feed, included only when the source can be verified.",
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
"id": "weather",
|
|
90
90
|
"type": "boolean",
|
|
91
91
|
"label": "Weather",
|
|
92
92
|
"default": True,
|
|
93
|
-
"help": "Today's weather from the location saved in Desktop or your residence in the profile.",
|
|
93
|
+
"help": "Today's weather from the location saved in Desktop or your residence in the profile, included only when the forecast can be verified.",
|
|
94
94
|
},
|
|
95
95
|
],
|
|
96
96
|
},
|
|
@@ -348,8 +348,12 @@ def format_automation_preferences_prompt_block(name_or_path: str) -> str:
|
|
|
348
348
|
return (
|
|
349
349
|
"\n== STRUCTURED CONTENT PREFERENCES FOR THIS AUTOMATION ==\n"
|
|
350
350
|
f"{compact}\n"
|
|
351
|
+
"Morning briefing intent: act like a professional personal assistant preparing the operator for the day. "
|
|
352
|
+
"Do not merely list available records; filter, rank, and explain what deserves attention first.\n"
|
|
353
|
+
"Adapt the emphasis from the operator profile, role, recent activity, and context. "
|
|
354
|
+
"Do not ask the user to choose a user type manually and do not assume a profession unless the context supports it.\n"
|
|
351
355
|
"Use these preferences to decide what to include, omit, and emphasize. "
|
|
352
|
-
"Disabled/unavailable data sources must not be invented.\n"
|
|
356
|
+
"Disabled/unavailable data sources must not be invented; news and weather require verified collected data.\n"
|
|
353
357
|
)
|
|
354
358
|
|
|
355
359
|
|
package/src/cognitive/_core.py
CHANGED
|
@@ -666,6 +666,9 @@ def _get_reranker():
|
|
|
666
666
|
"""Lazy-load cross-encoder reranking model."""
|
|
667
667
|
global _reranker
|
|
668
668
|
if _reranker is None:
|
|
669
|
+
if _model_download_disabled():
|
|
670
|
+
_reranker = False
|
|
671
|
+
return None
|
|
669
672
|
try:
|
|
670
673
|
from local_models import build_fastembed_reranker
|
|
671
674
|
|
|
@@ -418,6 +418,10 @@ def _touch_existing_followup(
|
|
|
418
418
|
date: str = "",
|
|
419
419
|
reasoning_note: str = "",
|
|
420
420
|
status: str = "",
|
|
421
|
+
verification: str = "",
|
|
422
|
+
priority: str = "",
|
|
423
|
+
internal: object = None,
|
|
424
|
+
owner: str = "",
|
|
421
425
|
) -> dict:
|
|
422
426
|
cols = _table_columns(NEXO_DB, "followups")
|
|
423
427
|
if not cols:
|
|
@@ -433,6 +437,15 @@ def _touch_existing_followup(
|
|
|
433
437
|
desired_status = (status or "").strip()
|
|
434
438
|
if desired_status and "status" in cols and desired_status != str(existing.get("status", "") or ""):
|
|
435
439
|
updates["status"] = desired_status
|
|
440
|
+
existing_verification = str(existing.get("verification", "") or "").strip()
|
|
441
|
+
if verification and "verification" in cols and not existing_verification:
|
|
442
|
+
updates["verification"] = verification
|
|
443
|
+
if priority and "priority" in cols and priority != str(existing.get("priority", "") or ""):
|
|
444
|
+
updates["priority"] = priority
|
|
445
|
+
if internal is not None and "internal" in cols:
|
|
446
|
+
updates["internal"] = 1 if str(internal).strip().lower() in {"1", "true", "yes", "on"} else 0
|
|
447
|
+
if owner and "owner" in cols and owner != str(existing.get("owner", "") or ""):
|
|
448
|
+
updates["owner"] = owner
|
|
436
449
|
note = reasoning_note or "Deep Sleep matched this followup semantically."
|
|
437
450
|
changed = False
|
|
438
451
|
if updates:
|
|
@@ -693,12 +706,52 @@ def add_learning(category: str, title: str, content: str) -> dict:
|
|
|
693
706
|
return {"success": False, "error": str(e)}
|
|
694
707
|
|
|
695
708
|
|
|
696
|
-
def
|
|
709
|
+
def _default_followup_date(date: str, *, status: str = "") -> str:
|
|
710
|
+
clean = str(date or "").strip()
|
|
711
|
+
if clean or str(status or "").strip().lower() == "archived":
|
|
712
|
+
return clean
|
|
713
|
+
return (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _default_followup_verification(description: str, verification: str = "") -> str:
|
|
717
|
+
clean = str(verification or "").strip()
|
|
718
|
+
if clean:
|
|
719
|
+
return clean
|
|
720
|
+
description_text = str(description or "").strip()
|
|
721
|
+
if not description_text:
|
|
722
|
+
return "Verify the Deep Sleep followup has a concrete deliverable or close it as invalid."
|
|
723
|
+
return f"Verify completion evidence for: {description_text[:180]}"
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _normalize_followup_priority(priority: str = "", impact: str = "") -> str:
|
|
727
|
+
clean = str(priority or "").strip().lower()
|
|
728
|
+
if clean in {"critical", "high", "medium", "low"}:
|
|
729
|
+
return clean
|
|
730
|
+
impact_clean = str(impact or "").strip().lower()
|
|
731
|
+
if impact_clean in {"critical", "high", "medium", "low"}:
|
|
732
|
+
return impact_clean
|
|
733
|
+
return "medium"
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def create_followup(
|
|
737
|
+
description: str,
|
|
738
|
+
date: str = "",
|
|
739
|
+
reasoning_note: str = "",
|
|
740
|
+
status: str = "PENDING",
|
|
741
|
+
verification: str = "",
|
|
742
|
+
priority: str = "",
|
|
743
|
+
internal: object = 1,
|
|
744
|
+
owner: str = "agent",
|
|
745
|
+
) -> dict:
|
|
697
746
|
"""Create a followup in nexo.db. Returns result dict."""
|
|
698
747
|
if not NEXO_DB.exists():
|
|
699
748
|
return {"success": False, "error": "nexo.db not found"}
|
|
700
749
|
try:
|
|
701
750
|
desired_status = (status or "PENDING").strip() or "PENDING"
|
|
751
|
+
desired_date = _default_followup_date(date, status=desired_status)
|
|
752
|
+
desired_verification = _default_followup_verification(description, verification)
|
|
753
|
+
desired_priority = _normalize_followup_priority(priority)
|
|
754
|
+
desired_owner = (owner or "agent").strip() or "agent"
|
|
702
755
|
is_abandoned = description.strip().startswith("[Abandoned]")
|
|
703
756
|
if not is_abandoned:
|
|
704
757
|
matched = _find_similar_followup(description)
|
|
@@ -706,9 +759,13 @@ def create_followup(description: str, date: str = "", reasoning_note: str = "",
|
|
|
706
759
|
return _touch_existing_followup(
|
|
707
760
|
matched,
|
|
708
761
|
description=description,
|
|
709
|
-
date=
|
|
762
|
+
date=desired_date,
|
|
710
763
|
reasoning_note=reasoning_note or "Deep Sleep matched this followup semantically.",
|
|
711
764
|
status=desired_status,
|
|
765
|
+
verification=desired_verification,
|
|
766
|
+
priority=desired_priority,
|
|
767
|
+
internal=internal,
|
|
768
|
+
owner=desired_owner,
|
|
712
769
|
)
|
|
713
770
|
|
|
714
771
|
# Generate a deterministic ID — content fingerprint, not security-sensitive.
|
|
@@ -718,19 +775,26 @@ def create_followup(description: str, date: str = "", reasoning_note: str = "",
|
|
|
718
775
|
return _touch_existing_followup(
|
|
719
776
|
existing,
|
|
720
777
|
description=description,
|
|
721
|
-
date=
|
|
778
|
+
date=desired_date,
|
|
722
779
|
reasoning_note=reasoning_note or "Deep Sleep revisited this deterministic followup.",
|
|
723
780
|
status=desired_status,
|
|
781
|
+
verification=desired_verification,
|
|
782
|
+
priority=desired_priority,
|
|
783
|
+
internal=internal,
|
|
784
|
+
owner=desired_owner,
|
|
724
785
|
)
|
|
725
786
|
|
|
726
787
|
followup_result = nexo_db.create_followup(
|
|
727
788
|
id=fid,
|
|
728
789
|
description=description,
|
|
729
|
-
date=
|
|
730
|
-
verification=
|
|
790
|
+
date=desired_date or None,
|
|
791
|
+
verification=desired_verification,
|
|
731
792
|
status=desired_status,
|
|
732
793
|
reasoning=reasoning_note or "Deep Sleep v2 overnight analysis",
|
|
733
794
|
recurrence=None,
|
|
795
|
+
priority=desired_priority,
|
|
796
|
+
internal=internal,
|
|
797
|
+
owner=desired_owner,
|
|
734
798
|
)
|
|
735
799
|
if followup_result.get("error"):
|
|
736
800
|
return {"success": False, "error": followup_result["error"]}
|
|
@@ -2113,10 +2177,15 @@ def apply_action(action: dict, run_id: str) -> dict:
|
|
|
2113
2177
|
log_entry["details"] = result
|
|
2114
2178
|
|
|
2115
2179
|
elif action_type == "followup_create":
|
|
2180
|
+
description = content.get("description", content.get("title", ""))
|
|
2116
2181
|
result = create_followup(
|
|
2117
|
-
description=
|
|
2182
|
+
description=description,
|
|
2118
2183
|
date=content.get("date", ""),
|
|
2119
2184
|
reasoning_note=content.get("reasoning", content.get("why", "")),
|
|
2185
|
+
verification=content.get("verification", content.get("success_signal", "")),
|
|
2186
|
+
priority=_normalize_followup_priority(content.get("priority", ""), action.get("impact", "")),
|
|
2187
|
+
internal=content.get("internal", 1),
|
|
2188
|
+
owner=content.get("owner", "agent"),
|
|
2120
2189
|
)
|
|
2121
2190
|
log_entry["status"] = "applied" if result.get("success") else "error"
|
|
2122
2191
|
log_entry["details"] = result
|
|
@@ -190,6 +190,62 @@ def backfill_engineering_actions(payload: dict) -> dict:
|
|
|
190
190
|
return payload
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
def _compact_action_title(action: dict) -> str:
|
|
194
|
+
content = action.get("content") if isinstance(action, dict) else {}
|
|
195
|
+
if isinstance(content, dict):
|
|
196
|
+
return str(content.get("title") or content.get("description") or "").strip()
|
|
197
|
+
return str(content or "").strip()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def build_agent_start_packet(payload: dict, target_date: str) -> dict:
|
|
201
|
+
"""Build a compact handoff packet for the next interactive agent startup."""
|
|
202
|
+
agenda = payload.get("morning_agenda") if isinstance(payload, dict) else []
|
|
203
|
+
packets = payload.get("context_packets") if isinstance(payload, dict) else []
|
|
204
|
+
actions = payload.get("actions") if isinstance(payload, dict) else []
|
|
205
|
+
|
|
206
|
+
review_items = []
|
|
207
|
+
if isinstance(actions, list):
|
|
208
|
+
for action in actions:
|
|
209
|
+
if not isinstance(action, dict):
|
|
210
|
+
continue
|
|
211
|
+
if action.get("action_class") != "draft_for_morning":
|
|
212
|
+
continue
|
|
213
|
+
title = _compact_action_title(action)
|
|
214
|
+
if title:
|
|
215
|
+
review_items.append(
|
|
216
|
+
{
|
|
217
|
+
"title": title,
|
|
218
|
+
"action_type": action.get("action_type", ""),
|
|
219
|
+
"impact": action.get("impact", ""),
|
|
220
|
+
"confidence": action.get("confidence", 0),
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
"date": str(payload.get("date") or target_date),
|
|
226
|
+
"generated_at": datetime.now().isoformat(),
|
|
227
|
+
"source": "deep-sleep/synthesize",
|
|
228
|
+
"summary": str(payload.get("summary") or "").strip(),
|
|
229
|
+
"agenda": agenda[:5] if isinstance(agenda, list) else [],
|
|
230
|
+
"context_packets": packets[:5] if isinstance(packets, list) else [],
|
|
231
|
+
"review_items": review_items[:5],
|
|
232
|
+
"counts": {
|
|
233
|
+
"actions": len(actions) if isinstance(actions, list) else 0,
|
|
234
|
+
"agenda": len(agenda) if isinstance(agenda, list) else 0,
|
|
235
|
+
"context_packets": len(packets) if isinstance(packets, list) else 0,
|
|
236
|
+
"review_items": len(review_items),
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def write_agent_start_packet(payload: dict, target_date: str) -> Path:
|
|
242
|
+
"""Persist a startup-ready Deep Sleep packet next to synthesis outputs."""
|
|
243
|
+
packet = build_agent_start_packet(payload, target_date)
|
|
244
|
+
packet_file = DEEP_SLEEP_DIR / f"{target_date}-agent-start-packet.json"
|
|
245
|
+
packet_file.write_text(json.dumps(packet, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
246
|
+
return packet_file
|
|
247
|
+
|
|
248
|
+
|
|
193
249
|
def main():
|
|
194
250
|
target_date = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
|
|
195
251
|
|
|
@@ -226,7 +282,9 @@ def main():
|
|
|
226
282
|
output_file = DEEP_SLEEP_DIR / f"{target_date}-synthesis.json"
|
|
227
283
|
with open(output_file, "w") as f:
|
|
228
284
|
json.dump(output, f, indent=2, ensure_ascii=False)
|
|
285
|
+
packet_file = write_agent_start_packet(output, target_date)
|
|
229
286
|
print(f"[synthesize] Output: {output_file}")
|
|
287
|
+
print(f"[synthesize] Agent start packet: {packet_file}")
|
|
230
288
|
return
|
|
231
289
|
|
|
232
290
|
# Build prompt
|
|
@@ -288,6 +346,7 @@ def main():
|
|
|
288
346
|
output_file = DEEP_SLEEP_DIR / f"{target_date}-synthesis.json"
|
|
289
347
|
with open(output_file, "w") as f:
|
|
290
348
|
json.dump(parsed, f, indent=2, ensure_ascii=False)
|
|
349
|
+
packet_file = write_agent_start_packet(parsed, target_date)
|
|
291
350
|
|
|
292
351
|
n_actions = len(parsed.get("actions", []))
|
|
293
352
|
n_patterns = len(parsed.get("cross_session_patterns", []))
|
|
@@ -300,6 +359,7 @@ def main():
|
|
|
300
359
|
print(f" Morning agenda items: {n_agenda}")
|
|
301
360
|
print(f" Context packets: {n_packets}")
|
|
302
361
|
print(f"[synthesize] Output: {output_file}")
|
|
362
|
+
print(f"[synthesize] Agent start packet: {packet_file}")
|
|
303
363
|
|
|
304
364
|
except AutomationBackendUnavailableError as exc:
|
|
305
365
|
print(f"[synthesize] Automation backend unavailable: {exc}", file=sys.stderr)
|
|
@@ -63,7 +63,10 @@ fi
|
|
|
63
63
|
|
|
64
64
|
# Phase 2: Extract findings per session (configured automation backend)
|
|
65
65
|
log "Phase 2: Extracting findings from $SESSIONS sessions..."
|
|
66
|
-
python3 "$SCRIPT_DIR/deep-sleep/extract.py" "$RUN_ID" >> "$LOG_DIR/deep-sleep.log" 2>&1
|
|
66
|
+
if ! python3 "$SCRIPT_DIR/deep-sleep/extract.py" "$RUN_ID" >> "$LOG_DIR/deep-sleep.log" 2>&1; then
|
|
67
|
+
log "Extraction failed. Watermark NOT updated (will retry next run)."
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
67
70
|
|
|
68
71
|
if [ ! -f "$DEEP_SLEEP_DIR/$RUN_ID-extractions.json" ]; then
|
|
69
72
|
log "Extraction failed. Watermark NOT updated (will retry next run)."
|
|
@@ -72,16 +75,22 @@ fi
|
|
|
72
75
|
|
|
73
76
|
# Phase 3: Cross-session synthesis (configured automation backend, one call)
|
|
74
77
|
log "Phase 3: Synthesizing cross-session findings..."
|
|
75
|
-
python3 "$SCRIPT_DIR/deep-sleep/synthesize.py" "$RUN_ID" >> "$LOG_DIR/deep-sleep.log" 2>&1
|
|
78
|
+
if ! python3 "$SCRIPT_DIR/deep-sleep/synthesize.py" "$RUN_ID" >> "$LOG_DIR/deep-sleep.log" 2>&1; then
|
|
79
|
+
log "Synthesis failed. Watermark NOT updated (will retry next run)."
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
76
82
|
|
|
77
83
|
if [ ! -f "$DEEP_SLEEP_DIR/$RUN_ID-synthesis.json" ]; then
|
|
78
|
-
log "Synthesis
|
|
79
|
-
|
|
84
|
+
log "Synthesis output missing. Watermark NOT updated (will retry next run)."
|
|
85
|
+
exit 1
|
|
80
86
|
fi
|
|
81
87
|
|
|
82
88
|
# Phase 4: Apply findings
|
|
83
89
|
log "Phase 4: Applying findings..."
|
|
84
|
-
python3 "$SCRIPT_DIR/deep-sleep/apply_findings.py" "$RUN_ID" >> "$LOG_DIR/deep-sleep.log" 2>&1
|
|
90
|
+
if ! python3 "$SCRIPT_DIR/deep-sleep/apply_findings.py" "$RUN_ID" >> "$LOG_DIR/deep-sleep.log" 2>&1; then
|
|
91
|
+
log "Apply failed. Watermark NOT updated (will retry next run)."
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
85
94
|
|
|
86
95
|
# Update watermark on success
|
|
87
96
|
echo "$UNTIL" > "$WATERMARK_FILE"
|
|
@@ -63,6 +63,11 @@ COMPRESSED_MEMORIES_DIR = BRAIN_DIR / "compressed_memories"
|
|
|
63
63
|
HEARTBEAT_LOG = COORD_DIR / "heartbeat-log.json"
|
|
64
64
|
REFLECTION_LOG = COORD_DIR / "reflection-log.json"
|
|
65
65
|
SLEEP_LOG = COORD_DIR / "sleep-log.json"
|
|
66
|
+
SLEEP_HEALTH_FILE = COORD_DIR / "sleep-health.json"
|
|
67
|
+
LEARNINGS_DUMP_FILE = COORD_DIR / "sleep-learnings-dump.json"
|
|
68
|
+
LEARNINGS_CHUNKS_DIR = COORD_DIR / "sleep-learnings-chunks"
|
|
69
|
+
MIN_LEARNING_COVERAGE_PCT = 95.0
|
|
70
|
+
LEARNING_CHUNK_MAX_CHARS = 50000
|
|
66
71
|
|
|
67
72
|
MEMORY_MD = paths.memory_dir() / "MEMORY.md"
|
|
68
73
|
NEXO_DB = paths.db_path()
|
|
@@ -209,6 +214,41 @@ def append_sleep_log(entry: dict):
|
|
|
209
214
|
save_json(SLEEP_LOG, entries)
|
|
210
215
|
|
|
211
216
|
|
|
217
|
+
def write_sleep_health(
|
|
218
|
+
run_log: dict,
|
|
219
|
+
state: dict,
|
|
220
|
+
*,
|
|
221
|
+
status: str,
|
|
222
|
+
error: str = "",
|
|
223
|
+
actions: dict | None = None,
|
|
224
|
+
) -> dict:
|
|
225
|
+
"""Write a small machine-readable health file for startup/briefings."""
|
|
226
|
+
stage_b = run_log.get("stage_b") if isinstance(run_log, dict) else {}
|
|
227
|
+
if not isinstance(stage_b, dict):
|
|
228
|
+
stage_b = {}
|
|
229
|
+
coverage = {}
|
|
230
|
+
if isinstance(actions, dict):
|
|
231
|
+
coverage = actions.get("coverage") or {}
|
|
232
|
+
if not coverage and isinstance(stage_b, dict):
|
|
233
|
+
coverage = stage_b.get("coverage") or {}
|
|
234
|
+
|
|
235
|
+
health = {
|
|
236
|
+
"date": str(TODAY),
|
|
237
|
+
"status": status,
|
|
238
|
+
"generated_at": datetime.now().isoformat(),
|
|
239
|
+
"error": error,
|
|
240
|
+
"learnings_total": len(state.get("learnings", []) if isinstance(state, dict) else []),
|
|
241
|
+
"memory_md_lines": state.get("memory_md_lines", 0) if isinstance(state, dict) else 0,
|
|
242
|
+
"old_observations": state.get("claude_mem_old", 0) if isinstance(state, dict) else 0,
|
|
243
|
+
"stage_a": run_log.get("stage_a") if isinstance(run_log, dict) else None,
|
|
244
|
+
"stage_b": stage_b,
|
|
245
|
+
"coverage": coverage,
|
|
246
|
+
"last_run_marked_complete": status == "ok",
|
|
247
|
+
}
|
|
248
|
+
save_json(SLEEP_HEALTH_FILE, health)
|
|
249
|
+
return health
|
|
250
|
+
|
|
251
|
+
|
|
212
252
|
# ─── Stage A: Mechanical cleanup (UNCHANGED from v1) ─────────────────────────
|
|
213
253
|
|
|
214
254
|
def stage_a_cleanup() -> dict:
|
|
@@ -388,13 +428,108 @@ def should_dream(state: dict) -> bool:
|
|
|
388
428
|
)
|
|
389
429
|
|
|
390
430
|
|
|
431
|
+
def write_learning_context(state: dict) -> dict:
|
|
432
|
+
"""Persist all learnings to files so Stage B never receives a truncated list."""
|
|
433
|
+
learnings = state.get("learnings", []) if isinstance(state, dict) else []
|
|
434
|
+
LEARNINGS_CHUNKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
435
|
+
|
|
436
|
+
# Remove stale chunks for today's run before rewriting them.
|
|
437
|
+
today_prefix = f"{TODAY}-chunk-"
|
|
438
|
+
for old_chunk in LEARNINGS_CHUNKS_DIR.glob(f"{today_prefix}*.json"):
|
|
439
|
+
try:
|
|
440
|
+
old_chunk.unlink()
|
|
441
|
+
except Exception:
|
|
442
|
+
pass
|
|
443
|
+
|
|
444
|
+
chunks: list[list[dict]] = []
|
|
445
|
+
current: list[dict] = []
|
|
446
|
+
current_chars = 2
|
|
447
|
+
for learning in learnings:
|
|
448
|
+
rendered = json.dumps(learning, ensure_ascii=False, separators=(",", ":"))
|
|
449
|
+
if current and current_chars + len(rendered) + 1 > LEARNING_CHUNK_MAX_CHARS:
|
|
450
|
+
chunks.append(current)
|
|
451
|
+
current = []
|
|
452
|
+
current_chars = 2
|
|
453
|
+
current.append(learning)
|
|
454
|
+
current_chars += len(rendered) + 1
|
|
455
|
+
if current or not chunks:
|
|
456
|
+
chunks.append(current)
|
|
457
|
+
|
|
458
|
+
chunk_files: list[str] = []
|
|
459
|
+
for index, chunk in enumerate(chunks, start=1):
|
|
460
|
+
chunk_path = LEARNINGS_CHUNKS_DIR / f"{TODAY}-chunk-{index:03d}.json"
|
|
461
|
+
save_json(
|
|
462
|
+
chunk_path,
|
|
463
|
+
{
|
|
464
|
+
"date": str(TODAY),
|
|
465
|
+
"chunk_index": index,
|
|
466
|
+
"chunk_count": len(chunks),
|
|
467
|
+
"learnings_total_declared": len(learnings),
|
|
468
|
+
"learnings_in_chunk": len(chunk),
|
|
469
|
+
"learnings": chunk,
|
|
470
|
+
},
|
|
471
|
+
)
|
|
472
|
+
chunk_files.append(str(chunk_path))
|
|
473
|
+
|
|
474
|
+
coverage = {
|
|
475
|
+
"learnings_visible_count": len(learnings),
|
|
476
|
+
"learnings_total_declared": len(learnings),
|
|
477
|
+
"coverage_pct": 100.0 if learnings or len(learnings) == 0 else 0.0,
|
|
478
|
+
"source_file": str(LEARNINGS_DUMP_FILE),
|
|
479
|
+
"chunk_count": len(chunk_files),
|
|
480
|
+
}
|
|
481
|
+
save_json(
|
|
482
|
+
LEARNINGS_DUMP_FILE,
|
|
483
|
+
{
|
|
484
|
+
"date": str(TODAY),
|
|
485
|
+
"generated_at": datetime.now().isoformat(),
|
|
486
|
+
"coverage": coverage,
|
|
487
|
+
"chunk_files": chunk_files,
|
|
488
|
+
"learnings": learnings,
|
|
489
|
+
},
|
|
490
|
+
)
|
|
491
|
+
return {
|
|
492
|
+
"dump_file": str(LEARNINGS_DUMP_FILE),
|
|
493
|
+
"chunk_files": chunk_files,
|
|
494
|
+
"coverage": coverage,
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def validate_actions_coverage(actions: dict, state: dict) -> tuple[bool, str]:
|
|
499
|
+
"""Fail closed when Stage B did not inspect nearly all active learnings."""
|
|
500
|
+
expected_total = len(state.get("learnings", []) if isinstance(state, dict) else [])
|
|
501
|
+
if expected_total == 0:
|
|
502
|
+
return True, "no active learnings"
|
|
503
|
+
|
|
504
|
+
coverage = actions.get("coverage") if isinstance(actions, dict) else None
|
|
505
|
+
if not isinstance(coverage, dict):
|
|
506
|
+
return False, "sleep-actions.json is missing coverage metadata"
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
visible_count = int(coverage.get("learnings_visible_count", 0) or 0)
|
|
510
|
+
except Exception:
|
|
511
|
+
visible_count = 0
|
|
512
|
+
try:
|
|
513
|
+
declared_total = int(coverage.get("learnings_total_declared", 0) or 0)
|
|
514
|
+
except Exception:
|
|
515
|
+
declared_total = 0
|
|
516
|
+
try:
|
|
517
|
+
coverage_pct = float(coverage.get("coverage_pct", 0.0) or 0.0)
|
|
518
|
+
except Exception:
|
|
519
|
+
coverage_pct = 0.0
|
|
520
|
+
|
|
521
|
+
if declared_total != expected_total:
|
|
522
|
+
return False, f"coverage declared {declared_total} learnings but state has {expected_total}"
|
|
523
|
+
if visible_count < expected_total:
|
|
524
|
+
return False, f"coverage only saw {visible_count}/{expected_total} learnings"
|
|
525
|
+
if coverage_pct < MIN_LEARNING_COVERAGE_PCT:
|
|
526
|
+
return False, f"coverage {coverage_pct:.1f}% is below {MIN_LEARNING_COVERAGE_PCT:.1f}%"
|
|
527
|
+
return True, "coverage ok"
|
|
528
|
+
|
|
529
|
+
|
|
391
530
|
def dream(state: dict) -> dict:
|
|
392
531
|
"""The brain dreams — CLI does the intelligent work."""
|
|
393
|
-
|
|
394
|
-
# Truncate learnings JSON if too large
|
|
395
|
-
learnings_json = json.dumps(state["learnings"], ensure_ascii=False, indent=1)
|
|
396
|
-
if len(learnings_json) > 15000:
|
|
397
|
-
learnings_json = learnings_json[:15000] + "\n... (truncated)"
|
|
532
|
+
learning_context = write_learning_context(state)
|
|
398
533
|
|
|
399
534
|
tasks = []
|
|
400
535
|
|
|
@@ -410,12 +545,30 @@ Write your findings to {COORD_DIR}/sleep-report.md with sections:
|
|
|
410
545
|
- "## Stale candidates" — IDs of learnings that may be obsolete
|
|
411
546
|
|
|
412
547
|
Also write a machine-readable file {COORD_DIR}/sleep-actions.json:
|
|
413
|
-
{{
|
|
548
|
+
{{
|
|
549
|
+
"archive_ids": [1, 2, 3],
|
|
550
|
+
"contradiction_pairs": [[4, 5]],
|
|
551
|
+
"stale_ids": [6, 7],
|
|
552
|
+
"coverage": {{
|
|
553
|
+
"learnings_visible_count": {len(state['learnings'])},
|
|
554
|
+
"learnings_total_declared": {len(state['learnings'])},
|
|
555
|
+
"coverage_pct": 100.0,
|
|
556
|
+
"source_file": "{learning_context['dump_file']}",
|
|
557
|
+
"chunk_files_read": {json.dumps(learning_context['chunk_files'], ensure_ascii=False)}
|
|
558
|
+
}}
|
|
559
|
+
}}
|
|
414
560
|
|
|
415
561
|
The wrapper will execute the actual DB operations based on this JSON.
|
|
562
|
+
The wrapper will fail closed if coverage is missing or below {MIN_LEARNING_COVERAGE_PCT:.0f}%.
|
|
563
|
+
|
|
564
|
+
LEARNINGS SOURCE:
|
|
565
|
+
Read the full learning list from {learning_context['dump_file']}.
|
|
566
|
+
If the file is too large for one read, read every chunk listed below and merge them by ID:
|
|
567
|
+
{json.dumps(learning_context['chunk_files'], ensure_ascii=False, indent=2)}
|
|
416
568
|
|
|
417
|
-
|
|
418
|
-
{
|
|
569
|
+
Do not infer duplicates/stale items from a partial list. If you cannot read at least
|
|
570
|
+
{MIN_LEARNING_COVERAGE_PCT:.0f}% of the declared learnings, write empty archive/stale arrays
|
|
571
|
+
and include coverage.blocking_reason explaining the read failure.""")
|
|
419
572
|
|
|
420
573
|
if state["memory_md_lines"] > 170:
|
|
421
574
|
tasks.append(f"""TASK 2: MEMORY.MD COMPRESSION ({state['memory_md_lines']} lines, limit 200)
|
|
@@ -461,20 +614,25 @@ The wrapper will handle the actual DB cleanup safely.""")
|
|
|
461
614
|
|
|
462
615
|
if result.returncode != 0:
|
|
463
616
|
log(f"Stage B: CLI error ({result.returncode}): {(result.stderr or '')[:300]}")
|
|
464
|
-
return {"error": result.returncode}
|
|
617
|
+
return {"error": result.returncode, "learning_context": learning_context}
|
|
465
618
|
|
|
466
619
|
log(f"Stage B: Dreaming complete. Output: {len(result.stdout or '')} chars")
|
|
467
|
-
return {
|
|
620
|
+
return {
|
|
621
|
+
"ok": True,
|
|
622
|
+
"output_len": len(result.stdout or ""),
|
|
623
|
+
"learning_context": learning_context,
|
|
624
|
+
"coverage": learning_context["coverage"],
|
|
625
|
+
}
|
|
468
626
|
|
|
469
627
|
except AutomationBackendUnavailableError as e:
|
|
470
628
|
log(f"Stage B: automation backend unavailable: {e}")
|
|
471
|
-
return {"error": "backend-unavailable"}
|
|
629
|
+
return {"error": "backend-unavailable", "learning_context": learning_context}
|
|
472
630
|
except subprocess.TimeoutExpired:
|
|
473
|
-
log("Stage B: CLI timed out (
|
|
474
|
-
return {"error": "timeout"}
|
|
631
|
+
log(f"Stage B: CLI timed out ({AUTOMATION_SUBPROCESS_TIMEOUT}s)")
|
|
632
|
+
return {"error": "timeout", "learning_context": learning_context}
|
|
475
633
|
except Exception as e:
|
|
476
634
|
log(f"Stage B: Exception: {e}")
|
|
477
|
-
return {"error": str(e)}
|
|
635
|
+
return {"error": str(e), "learning_context": learning_context}
|
|
478
636
|
|
|
479
637
|
|
|
480
638
|
def execute_dream_actions(actions: dict, state: dict):
|
|
@@ -573,8 +731,11 @@ def main():
|
|
|
573
731
|
start_phase = get_interrupted_phase()
|
|
574
732
|
|
|
575
733
|
run_log = {"date": str(TODAY), "started": TIMESTAMP,
|
|
576
|
-
"stage_a": None, "stage_b": None, "completed": None
|
|
734
|
+
"stage_a": None, "stage_b": None, "completed": None,
|
|
735
|
+
"marked_complete": False}
|
|
577
736
|
sleep_had_errors = False
|
|
737
|
+
sleep_error = ""
|
|
738
|
+
actions = None
|
|
578
739
|
|
|
579
740
|
# Stage A: Housekeeping (mechanical)
|
|
580
741
|
if start_phase == "stage_a":
|
|
@@ -598,22 +759,49 @@ def main():
|
|
|
598
759
|
log(f"Stage B: Dreaming failed ({dream_result['error']}). "
|
|
599
760
|
"Stage A cleanup completed successfully. Not marking catchup to allow retry.")
|
|
600
761
|
sleep_had_errors = True
|
|
762
|
+
sleep_error = str(dream_result["error"])
|
|
601
763
|
else:
|
|
602
764
|
# Stage B2: Execute actions from CLI output
|
|
603
765
|
actions_file = COORD_DIR / "sleep-actions.json"
|
|
604
766
|
if actions_file.exists():
|
|
605
767
|
try:
|
|
606
768
|
actions = json.loads(actions_file.read_text())
|
|
607
|
-
|
|
769
|
+
coverage_ok, coverage_reason = validate_actions_coverage(actions, state)
|
|
770
|
+
run_log["stage_b"]["actions_file"] = str(actions_file)
|
|
771
|
+
run_log["stage_b"]["coverage_ok"] = coverage_ok
|
|
772
|
+
run_log["stage_b"]["coverage_reason"] = coverage_reason
|
|
773
|
+
if not coverage_ok:
|
|
774
|
+
log(f"Stage B2: Refusing dream actions: {coverage_reason}")
|
|
775
|
+
sleep_had_errors = True
|
|
776
|
+
sleep_error = coverage_reason
|
|
777
|
+
else:
|
|
778
|
+
execute_dream_actions(actions, state)
|
|
608
779
|
except Exception as e:
|
|
609
780
|
log(f"Stage B2: Error executing actions: {e}")
|
|
781
|
+
sleep_had_errors = True
|
|
782
|
+
sleep_error = str(e)
|
|
783
|
+
else:
|
|
784
|
+
sleep_had_errors = True
|
|
785
|
+
sleep_error = f"missing actions file: {actions_file}"
|
|
786
|
+
run_log["stage_b"]["error"] = sleep_error
|
|
787
|
+
log(f"Stage B2: {sleep_error}")
|
|
610
788
|
else:
|
|
611
789
|
log("Brain is clean -- no dreaming needed.")
|
|
612
790
|
run_log["stage_b"] = {"skipped": True}
|
|
613
791
|
|
|
614
792
|
# Done
|
|
615
793
|
run_log["completed"] = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
794
|
+
if sleep_had_errors:
|
|
795
|
+
run_log["status"] = "failed"
|
|
796
|
+
write_sleep_health(run_log, state, status="failed", error=sleep_error, actions=actions)
|
|
797
|
+
append_sleep_log(run_log)
|
|
798
|
+
log("NEXO Sleep v2 failed during Stage B; not marking today complete so the next trigger retries.")
|
|
799
|
+
sys.exit(1)
|
|
800
|
+
|
|
616
801
|
mark_complete()
|
|
802
|
+
run_log["marked_complete"] = True
|
|
803
|
+
run_log["status"] = "ok"
|
|
804
|
+
write_sleep_health(run_log, state, status="ok", actions=actions)
|
|
617
805
|
append_sleep_log(run_log)
|
|
618
806
|
log(f"NEXO Sleep v2 complete at {run_log['completed']}")
|
|
619
807
|
|
package/src/tools_sessions.py
CHANGED
|
@@ -634,6 +634,192 @@ def _read_session_briefing_excerpt(max_lines: int = 6) -> tuple[str, str]:
|
|
|
634
634
|
return excerpt, str(briefing_path)
|
|
635
635
|
|
|
636
636
|
|
|
637
|
+
def _read_sleep_health_warning() -> tuple[list[str], str]:
|
|
638
|
+
"""Return a compact warning when nightly sleep failed or degraded."""
|
|
639
|
+
health_path = paths.coordination_dir() / "sleep-health.json"
|
|
640
|
+
try:
|
|
641
|
+
payload = json.loads(health_path.read_text(encoding="utf-8"))
|
|
642
|
+
except (FileNotFoundError, OSError, json.JSONDecodeError):
|
|
643
|
+
return [], str(health_path)
|
|
644
|
+
|
|
645
|
+
if not isinstance(payload, dict):
|
|
646
|
+
return [], str(health_path)
|
|
647
|
+
status = str(payload.get("status") or "").strip().lower()
|
|
648
|
+
if not status or status == "ok":
|
|
649
|
+
return [], str(health_path)
|
|
650
|
+
|
|
651
|
+
date_value = str(payload.get("date") or "").strip()
|
|
652
|
+
error = _safe_packet_text(payload.get("error") or "unknown", max_chars=180)
|
|
653
|
+
lines = [f"status={status} date={date_value or '?'} error={error}"]
|
|
654
|
+
|
|
655
|
+
coverage = payload.get("coverage") or {}
|
|
656
|
+
if isinstance(coverage, dict):
|
|
657
|
+
visible = coverage.get("learnings_visible_count")
|
|
658
|
+
total = coverage.get("learnings_total_declared")
|
|
659
|
+
pct = coverage.get("coverage_pct")
|
|
660
|
+
if visible is not None or total is not None or pct is not None:
|
|
661
|
+
lines.append(f"coverage={visible or 0}/{total or 0} ({pct or 0}%)")
|
|
662
|
+
|
|
663
|
+
return lines, str(health_path)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _latest_deep_sleep_synthesis() -> Path | None:
|
|
667
|
+
deep_sleep_dir = paths.operations_dir() / "deep-sleep"
|
|
668
|
+
try:
|
|
669
|
+
candidates = [
|
|
670
|
+
path
|
|
671
|
+
for path in deep_sleep_dir.glob("????-??-??-synthesis.json")
|
|
672
|
+
if path.is_file()
|
|
673
|
+
]
|
|
674
|
+
except OSError:
|
|
675
|
+
return None
|
|
676
|
+
if not candidates:
|
|
677
|
+
return None
|
|
678
|
+
return max(candidates, key=lambda path: (path.name[:10], path.stat().st_mtime))
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _latest_deep_sleep_start_packet() -> Path | None:
|
|
682
|
+
deep_sleep_dir = paths.operations_dir() / "deep-sleep"
|
|
683
|
+
try:
|
|
684
|
+
candidates = [
|
|
685
|
+
path
|
|
686
|
+
for path in deep_sleep_dir.glob("????-??-??-agent-start-packet.json")
|
|
687
|
+
if path.is_file()
|
|
688
|
+
]
|
|
689
|
+
except OSError:
|
|
690
|
+
return None
|
|
691
|
+
if not candidates:
|
|
692
|
+
return None
|
|
693
|
+
return max(candidates, key=lambda path: (path.name[:10], path.stat().st_mtime))
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _read_agent_start_packet(payload: dict, packet_path: Path) -> tuple[list[str], str]:
|
|
697
|
+
lines: list[str] = []
|
|
698
|
+
date_value = str(payload.get("date") or packet_path.name[:10]).strip()
|
|
699
|
+
summary = str(payload.get("summary") or "").strip()
|
|
700
|
+
if date_value:
|
|
701
|
+
lines.append(f"date={date_value}")
|
|
702
|
+
if summary:
|
|
703
|
+
lines.append(f"summary={_safe_packet_text(summary, max_chars=220)}")
|
|
704
|
+
|
|
705
|
+
agenda = payload.get("agenda") or []
|
|
706
|
+
if isinstance(agenda, list):
|
|
707
|
+
for item in agenda[:3]:
|
|
708
|
+
if not isinstance(item, dict):
|
|
709
|
+
continue
|
|
710
|
+
priority = str(item.get("priority") or "?").strip()
|
|
711
|
+
title = str(item.get("title") or "").strip()
|
|
712
|
+
description = str(item.get("description") or "").strip()
|
|
713
|
+
if title or description:
|
|
714
|
+
text = title if not description else f"{title} - {description}"
|
|
715
|
+
lines.append(f"agenda[{priority}]={_safe_packet_text(text, max_chars=220)}")
|
|
716
|
+
|
|
717
|
+
packets = payload.get("context_packets") or []
|
|
718
|
+
if isinstance(packets, list):
|
|
719
|
+
for packet in packets[:2]:
|
|
720
|
+
if not isinstance(packet, dict):
|
|
721
|
+
continue
|
|
722
|
+
topic = str(packet.get("topic") or "context").strip()
|
|
723
|
+
last_state = str(packet.get("last_state") or "").strip()
|
|
724
|
+
if topic or last_state:
|
|
725
|
+
text = topic if not last_state else f"{topic}: {last_state}"
|
|
726
|
+
lines.append(f"context={_safe_packet_text(text, max_chars=240)}")
|
|
727
|
+
files = packet.get("key_files") or []
|
|
728
|
+
if isinstance(files, list) and files:
|
|
729
|
+
rendered_files = ", ".join(str(file) for file in files[:4] if str(file).strip())
|
|
730
|
+
if rendered_files:
|
|
731
|
+
lines.append(f"files={_safe_packet_text(rendered_files, max_chars=220)}")
|
|
732
|
+
|
|
733
|
+
review_items = payload.get("review_items") or []
|
|
734
|
+
if isinstance(review_items, list):
|
|
735
|
+
for item in review_items[:2]:
|
|
736
|
+
if not isinstance(item, dict):
|
|
737
|
+
continue
|
|
738
|
+
title = str(item.get("title") or "").strip()
|
|
739
|
+
if title:
|
|
740
|
+
lines.append(f"review={_safe_packet_text(title, max_chars=220)}")
|
|
741
|
+
|
|
742
|
+
return lines[:10], str(packet_path)
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _read_deep_sleep_start_context() -> tuple[list[str], str]:
|
|
746
|
+
"""Summarize the latest Deep Sleep synthesis for new agent sessions."""
|
|
747
|
+
packet_path = _latest_deep_sleep_start_packet()
|
|
748
|
+
if packet_path is not None:
|
|
749
|
+
try:
|
|
750
|
+
packet_payload = json.loads(packet_path.read_text(encoding="utf-8"))
|
|
751
|
+
except (OSError, json.JSONDecodeError):
|
|
752
|
+
packet_payload = None
|
|
753
|
+
if isinstance(packet_payload, dict):
|
|
754
|
+
return _read_agent_start_packet(packet_payload, packet_path)
|
|
755
|
+
|
|
756
|
+
synthesis_path = _latest_deep_sleep_synthesis()
|
|
757
|
+
if synthesis_path is None:
|
|
758
|
+
return [], str(paths.operations_dir() / "deep-sleep")
|
|
759
|
+
|
|
760
|
+
try:
|
|
761
|
+
payload = json.loads(synthesis_path.read_text(encoding="utf-8"))
|
|
762
|
+
except (OSError, json.JSONDecodeError):
|
|
763
|
+
return [], str(synthesis_path)
|
|
764
|
+
if not isinstance(payload, dict):
|
|
765
|
+
return [], str(synthesis_path)
|
|
766
|
+
|
|
767
|
+
lines: list[str] = []
|
|
768
|
+
date_value = str(payload.get("date") or synthesis_path.name[:10]).strip()
|
|
769
|
+
summary = str(payload.get("summary") or "").strip()
|
|
770
|
+
if date_value:
|
|
771
|
+
lines.append(f"date={date_value}")
|
|
772
|
+
if summary:
|
|
773
|
+
lines.append(f"summary={_safe_packet_text(summary, max_chars=220)}")
|
|
774
|
+
|
|
775
|
+
agenda = payload.get("morning_agenda") or []
|
|
776
|
+
if isinstance(agenda, list):
|
|
777
|
+
for item in agenda[:3]:
|
|
778
|
+
if not isinstance(item, dict):
|
|
779
|
+
continue
|
|
780
|
+
priority = str(item.get("priority") or "?").strip()
|
|
781
|
+
title = str(item.get("title") or "").strip()
|
|
782
|
+
description = str(item.get("description") or "").strip()
|
|
783
|
+
if title or description:
|
|
784
|
+
text = title if not description else f"{title} - {description}"
|
|
785
|
+
lines.append(f"agenda[{priority}]={_safe_packet_text(text, max_chars=220)}")
|
|
786
|
+
|
|
787
|
+
packets = payload.get("context_packets") or []
|
|
788
|
+
if isinstance(packets, list):
|
|
789
|
+
for packet in packets[:2]:
|
|
790
|
+
if not isinstance(packet, dict):
|
|
791
|
+
continue
|
|
792
|
+
topic = str(packet.get("topic") or "context").strip()
|
|
793
|
+
last_state = str(packet.get("last_state") or "").strip()
|
|
794
|
+
if topic or last_state:
|
|
795
|
+
text = topic if not last_state else f"{topic}: {last_state}"
|
|
796
|
+
lines.append(f"context={_safe_packet_text(text, max_chars=240)}")
|
|
797
|
+
files = packet.get("key_files") or []
|
|
798
|
+
if isinstance(files, list) and files:
|
|
799
|
+
rendered_files = ", ".join(str(file) for file in files[:4] if str(file).strip())
|
|
800
|
+
if rendered_files:
|
|
801
|
+
lines.append(f"files={_safe_packet_text(rendered_files, max_chars=220)}")
|
|
802
|
+
|
|
803
|
+
actions = payload.get("actions") or []
|
|
804
|
+
if isinstance(actions, list):
|
|
805
|
+
review_count = 0
|
|
806
|
+
for action in actions:
|
|
807
|
+
if review_count >= 2 or not isinstance(action, dict):
|
|
808
|
+
continue
|
|
809
|
+
if action.get("action_class") != "draft_for_morning":
|
|
810
|
+
continue
|
|
811
|
+
content = action.get("content") or {}
|
|
812
|
+
if isinstance(content, dict):
|
|
813
|
+
title = str(content.get("title") or content.get("description") or "").strip()
|
|
814
|
+
else:
|
|
815
|
+
title = str(content or "").strip()
|
|
816
|
+
if title:
|
|
817
|
+
lines.append(f"review={_safe_packet_text(title, max_chars=220)}")
|
|
818
|
+
review_count += 1
|
|
819
|
+
|
|
820
|
+
return lines[:10], str(synthesis_path)
|
|
821
|
+
|
|
822
|
+
|
|
637
823
|
def handle_startup(
|
|
638
824
|
task: str = "Startup",
|
|
639
825
|
claude_session_id: str = "",
|
|
@@ -796,6 +982,22 @@ def handle_startup(
|
|
|
796
982
|
lines.append(f" {_safe_packet_text(raw_line)}")
|
|
797
983
|
lines.append(f" Full briefing: {_safe_packet_text(briefing_path)}")
|
|
798
984
|
|
|
985
|
+
sleep_health, sleep_health_path = _read_sleep_health_warning()
|
|
986
|
+
if sleep_health:
|
|
987
|
+
lines.append("")
|
|
988
|
+
lines.append("SLEEP HEALTH:")
|
|
989
|
+
for raw_line in sleep_health:
|
|
990
|
+
lines.append(f" {_safe_packet_text(raw_line)}")
|
|
991
|
+
lines.append(f" Full health: {_safe_packet_text(sleep_health_path)}")
|
|
992
|
+
|
|
993
|
+
start_context, start_context_path = _read_deep_sleep_start_context()
|
|
994
|
+
if start_context:
|
|
995
|
+
lines.append("")
|
|
996
|
+
lines.append("DEEP SLEEP CONTEXT:")
|
|
997
|
+
for raw_line in start_context:
|
|
998
|
+
lines.append(f" {_safe_packet_text(raw_line)}")
|
|
999
|
+
lines.append(f" Full synthesis: {_safe_packet_text(start_context_path)}")
|
|
1000
|
+
|
|
799
1001
|
try:
|
|
800
1002
|
from memory_layer_audit import audit_memory_layers, format_memory_layer_warnings
|
|
801
1003
|
|
|
@@ -4,6 +4,13 @@ Write the email using ONLY the facts present in the structured context below.
|
|
|
4
4
|
Use the operator's preferred language: [[operator_language]].
|
|
5
5
|
If the language value is invalid or unclear, use English.
|
|
6
6
|
|
|
7
|
+
Product intent:
|
|
8
|
+
- This is a start-of-day briefing, not a report dump. The operator should finish reading it knowing what matters, what changed, what can wait, and what needs a decision.
|
|
9
|
+
- Think like a professional personal assistant or chief of staff: filter noise, rank priorities, connect related items, and make the day feel prepared.
|
|
10
|
+
- Adapt the emphasis to the operator's real context: administrative work needs tasks, deadlines, email movement and missing inputs; executives need decisions, risks, money, people and blocked outcomes; technical users need incidents, deployments, regressions and dependencies; commercial users need leads, customers and follow-ups; regulated or clinical users need careful wording, pending actions and factual status only.
|
|
11
|
+
- Do not ask the operator to choose a user type. Infer the useful angle from the structured context, profile fields, recent activity and explicit preferences. If the context is thin, stay general and practical.
|
|
12
|
+
- Include news and weather only when verified collected data exists in the context. Never fabricate public news, forecasts, calendar events, emails or professional advice.
|
|
13
|
+
|
|
7
14
|
Hard rules:
|
|
8
15
|
- Do not invent achievements, blockers, meetings, messages, or external events.
|
|
9
16
|
- Do not mention source files, JSON, MCP, prompts, or internal implementation.
|