feat(tmux): add server death watchdog daemon and multi-socket approver enhancements
This commit is contained in:
@@ -285,13 +285,44 @@ def run_tmux_cmd(socket_path: str, *args: str, timeout: float = 3.0) -> Tuple[in
|
||||
|
||||
|
||||
def capture_pane_text(socket_path: str, pane_id: str, lines: int = 30) -> str:
|
||||
"""Capture recent lines from a pane."""
|
||||
rc, out, _ = run_tmux_cmd(socket_path, "capture-pane", "-p", "-t", pane_id, "-S", f"-{lines}")
|
||||
"""Capture recent lines from a pane, joining wrapped rows.
|
||||
|
||||
-J joins physical wrapped lines into logical lines so matching is
|
||||
width-independent: narrow panes wrap the same dialog onto more
|
||||
rows, which otherwise breaks cue/option regexes.
|
||||
"""
|
||||
rc, out, _ = run_tmux_cmd(socket_path, "capture-pane", "-p", "-J",
|
||||
"-t", pane_id, "-S", f"-{lines}")
|
||||
if rc == 0:
|
||||
return out
|
||||
return ""
|
||||
|
||||
|
||||
MUSE_COMMAND_HINTS = ("muse-bin", "muse-code")
|
||||
|
||||
|
||||
def should_defer_to_muse_watcher(socket_path: str, pane_id: str,
|
||||
current_command: str) -> bool:
|
||||
"""True when a per-pane muse watcher owns this pane.
|
||||
|
||||
Single-owner rule: muse_choice_watcher is authoritative for muse
|
||||
panes (stability + re-verify + once-per-prompt + decided-block
|
||||
guard). When its daemon is alive for this socket:pane, tmux must
|
||||
skip the pane entirely, or both daemons answer the same prompt
|
||||
within the same second ('11' + stray keys, observed live). Never
|
||||
raises: import or liveness failures mean no owner, handle here.
|
||||
"""
|
||||
try:
|
||||
cmd = current_command or ""
|
||||
if not any(h in cmd for h in MUSE_COMMAND_HINTS):
|
||||
return False
|
||||
import muse_choice_watcher as mcw
|
||||
alive = getattr(mcw, "watcher_alive", mcw.is_running)
|
||||
return alive(socket_path, pane_id) is not None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def gather_tmux_tally(state: Optional[AutoApproverState] = None) -> TmuxWorkerTally:
|
||||
"""Scan all sockets and build a comprehensive tally of tmux workers."""
|
||||
if state is None:
|
||||
@@ -497,6 +528,9 @@ class AutoApproverRunner:
|
||||
for p in tally.panes:
|
||||
if not p.auto_approve:
|
||||
continue
|
||||
if should_defer_to_muse_watcher(p.socket, p.pane_id,
|
||||
p.current_command):
|
||||
continue
|
||||
|
||||
text = capture_pane_text(p.socket, p.pane_id, lines=30)
|
||||
if not text:
|
||||
@@ -516,9 +550,12 @@ class AutoApproverRunner:
|
||||
continue
|
||||
|
||||
if verdict.matched and verdict.key:
|
||||
# Deduplicate identical prompt to avoid infinite loop
|
||||
# Deduplicate identical prompt to avoid infinite loop.
|
||||
# Keyed by socket:pane: bare pane ids repeat on every
|
||||
# tmux socket, so %1 on pip must not suppress %1 on opm.
|
||||
sig = hashlib.sha1(f"{verdict.rule_id}:{verdict.excerpt}".encode()).hexdigest()
|
||||
last_time, last_sig = self.recent_signatures.get(p.pane_id, (0, ""))
|
||||
dedup_key = "%s:%s" % (p.socket, p.pane_id)
|
||||
last_time, last_sig = self.recent_signatures.get(dedup_key, (0, ""))
|
||||
if last_sig == sig and (time.time() - last_time) < 15.0:
|
||||
continue # already handled recently
|
||||
|
||||
@@ -544,7 +581,7 @@ class AutoApproverRunner:
|
||||
success = True # dry-run simulated
|
||||
|
||||
now = time.time()
|
||||
self.recent_signatures[p.pane_id] = (now, sig)
|
||||
self.recent_signatures[dedup_key] = (now, sig)
|
||||
self.approval_counts.append(now)
|
||||
|
||||
event = {
|
||||
|
||||
Reference in New Issue
Block a user