feat(hybrid-gateway): integrate muse-cli with Cloudflare netns isolation, symmetric sidechat routing, and 646-pip sync unblock

This commit is contained in:
operator
2026-10-04 22:54:01 +00:00
parent 3d5fbe5aeb
commit 1a271b1bbd
60 changed files with 5068 additions and 55 deletions
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""
Side-chat to main-chat work siphon — siphon action.
When detection fires, post a summary to main chat with:
- Category badge
- One-line summary (never full message text)
- Link back to the source side chat thread
- Confidence score (for transparency)
Safety:
- Rate limited (max N siphons per hour per thread)
- Never posts full message content
- Respects opt-out registry
- Deduplicates (same message_id never siphoned twice)
"""
import time
from dataclasses import dataclass, field
from typing import Callable, Optional
from detect import SiphonHit, is_opted_out
# --- Follow-up modulation ---
#
# Wire the follow-up modulation table into the siphon so each hit gets
# the right follow-up policy:
# ALERT / BLOCKER / DECISION -> tracked, fast fuse for ALERT/BLOCKER
# COMPLETED / MILESTONE -> untracked (no nudge budget burned)
#
# modulate.py must be landed on bl before this runs (rollout step 1).
# If the import fails we degrade to the old behavior: post the summary
# with no follow-up tags (fail-closed toward visibility, not tracking).
try:
from modulate import for_siphon_hit, render_tags
_MODULATION_AVAILABLE = True
except ImportError: # pragma: no cover - deploy keeps modulate.py present
_MODULATION_AVAILABLE = False
for_siphon_hit = None
render_tags = None
def policy_for_hit(hit: SiphonHit):
"""Follow-up policy for a siphon hit, or None when untracked.
COMPLETED / MILESTONE hits return None (post the summary, create no
follow-up record). ALERT / BLOCKER / DECISION return a Policy whose
tags render into the canonical bracket vocabulary.
"""
if not _MODULATION_AVAILABLE:
return None
return for_siphon_hit(hit.category)
def is_tracked(hit: SiphonHit) -> bool:
"""True when this hit should create a follow-up record.
Callers that route tracked posts through dm.py --expect-reply (so a
dm_followup record is actually created) can use this to choose the
post path. Untracked hits post as plain summaries.
"""
return policy_for_hit(hit) is not None
# --- Rate limiting ---
@dataclass
class RateLimiter:
max_per_hour: int = 5
_timestamps: dict = field(default_factory=dict) # thread_id -> [ts, ...]
def allow(self, thread_id: str) -> bool:
now = time.time()
stamps = self._timestamps.get(thread_id, [])
# Prune older than 1 hour
stamps = [s for s in stamps if now - s < 3600]
if len(stamps) >= self.max_per_hour:
return False
stamps.append(now)
self._timestamps[thread_id] = stamps
return True
# --- Deduplication ---
_siphoned_ids: set = set()
def already_siphoned(message_id: str) -> bool:
return message_id in _siphoned_ids
def mark_siphoned(message_id: str):
_siphoned_ids.add(message_id)
# --- Siphon action ---
CATEGORY_EMOJI = {
"COMPLETED": "✅",
"BLOCKER": "🚧",
"DECISION": "❓",
"ALERT": "🚨",
"MILESTONE": "🎯",
}
def format_siphon(hit: SiphonHit, agent_name: str = "sidechat") -> str:
"""
Format a siphon message for main chat.
Never includes full message text — summary + link only.
"""
emoji = CATEGORY_EMOJI.get(hit.category, "📋")
thread_url = f"https://muse.ai/thread/{hit.thread_id}"
return (
f"{emoji} [{hit.category}] from {agent_name} side chat\n"
f"{hit.summary}\n"
f"→ {thread_url}\n"
f"(confidence {hit.confidence:.0%})"
)
def siphon(hit: SiphonHit,
agent_name: str,
post_to_main: Callable[[str], bool],
limiter: Optional[RateLimiter] = None) -> bool:
"""
Execute the siphon: post summary to main chat.
post_to_main: callable that posts text to main chat, returns True on success.
Returns True if siphoned, False if suppressed.
"""
# Safety checks
if is_opted_out(hit.thread_id):
return False
if already_siphoned(hit.message_id):
return False
lim = limiter or RateLimiter()
if not lim.allow(hit.thread_id):
return False
text = format_siphon(hit, agent_name)
# Follow-up modulation: tracked hits (ALERT/BLOCKER/DECISION) get
# the canonical follow-up tags appended — [reply:expected],
# [reply:timeout=N], [reply:nudges=N], [reply:escalate=X], and
# [input:siphon] for the audit trail. Untracked hits
# (COMPLETED/MILESTONE) post as plain summaries.
policy = policy_for_hit(hit)
if policy is not None:
text = text + "\n" + render_tags(policy)
ok = post_to_main(text)
if ok:
mark_siphoned(hit.message_id)
return ok