feat(supervision): add choice watcher daemon, HTTPS spec docs, and test suites

- bin/muse_choice_watcher.py + systemd/muse-choices-reconcile.*: automatic choice answering and timer reconciliation
- bin/digest.py: fleet log and health summarization
- docs/BOX-*-HTTPS.md: comprehensive HTTPS execution contracts and API documentation
- docs/MUSE-CHOICES-POLICY.md & docs/SUPERVISION-SPEC.md: autonomous execution specs
- tests/test_*.py: unit test suites for HTTPS API, choice watcher, fleet heal, and swarm pruning
This commit is contained in:
operator
2026-10-07 00:25:46 +00:00
parent 04339bad14
commit 0065d11e97
29 changed files with 6419 additions and 7 deletions
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""
Side-chat work digest — INTEGRATOR minimal version (agent 4 of 5 never
delivered its digest/throttle design within the window).
Purpose: stop the per-message ✅ COMPLETED relay flood. COMPLETED hits are
batched here and emitted as ONE periodic digest instead of N main-chat
messages. ALERT / BLOCKER / DECISION still relay individually via
siphon() — urgency is never batched.
Interface (what agent 4's full design should remain compatible with):
- buffer = DigestBuffer(max_items=20, max_age_s=3600)
- buffer.add(hit) -> None
- buffer.flush() -> Optional[str] (formatted digest, clears buffer)
- flush_digest() -> Optional[str] (module-level singleton convenience)
Reversible: to restore per-message COMPLETED relays, route COMPLETED back
through siphon() in monitor.py and ignore this module.
"""
import time
from typing import List, Optional
try:
from detect import SiphonHit
except ImportError: # pragma: no cover
SiphonHit = object
class DigestBuffer:
"""Batch COMPLETED hits; flush() renders one digest message."""
def __init__(self, max_items: int = 20, max_age_s: int = 3600):
self.max_items = max_items
self.max_age_s = max_age_s
self._items: List[tuple] = [] # (ts, SiphonHit)
def add(self, hit) -> None:
now = time.time()
# Prune items older than max_age_s on every add (bounded memory).
self._items = [(ts, h) for ts, h in self._items
if now - ts < self.max_age_s]
self._items.append((now, hit))
# Bound the buffer; oldest evicted first.
self._items = self._items[-self.max_items:]
def __len__(self) -> int:
return len(self._items)
def flush(self) -> Optional[str]:
"""Render and clear. Returns None when there's nothing to digest."""
if not self._items:
return None
lines = ["📦 [DIGEST] completions from side chats "
f"({len(self._items)} item(s))"]
for _, hit in self._items:
author = getattr(hit, "author", "") or "?"
url = f"https://muse.ai/thread/{hit.thread_id}"
lines.append(f"• {hit.summary} — {author} ({url})")
self._items = []
return "\n".join(lines)
# Module-level singleton: the monitor loop shares one buffer per process.
_default_buffer = DigestBuffer()
def get_buffer() -> DigestBuffer:
return _default_buffer
def flush_digest() -> Optional[str]:
"""Flush the process-wide digest buffer. None if empty."""
return _default_buffer.flush()