feat(subagent): implement subagent lifecycle tracking and core loop reactive wakeup

- Implement bin/subagent_tracker.py with session registry in subagent-sessions.json
- Register spawned sessions automatically in super-cli.py cmd_deploy
- Add check_subagents() to bin/self_main_loop.py with thread updated watermark comparison
- Add 15-minute stall watchdog with automated parent alerting
- Support 'box subagent spawn' top-level command alias in super-cli.py
- Add symmetrical pip-opm sidechat routing in job-sidechats.json
This commit is contained in:
operator
2026-10-04 23:40:04 +00:00
parent d6ca600394
commit f60394cd9b
4 changed files with 228 additions and 3 deletions
+89 -2
View File
@@ -378,6 +378,85 @@ def compose_dual_digest(agent, main_new, sc_activity):
return "\n".join(lines)[:DIGEST_MAX]
def check_subagents(agent, cfg, threads_meta):
"""Inspect active child subagents spawned by agent.
Returns list of alert strings to deliver to the parent's prompting sidechat:
- New output from subagent: prompts parent with snippet and thread view command.
- 15-minute stall: warns parent that subagent has had no activity.
"""
alerts = []
try:
import subagent_tracker
import muse_hybrid
active = subagent_tracker.get_active_sessions(parent=agent)
if not active or not threads_meta:
return alerts
meta_by_id = {t.get("session_id"): t for t in threads_meta if isinstance(t, dict)}
now = datetime.now(timezone.utc)
for s in active:
sid = s.get("session_id")
title = s.get("title") or "subagent"
last_wm = s.get("last_watermark")
t_meta = meta_by_id.get(sid)
if not t_meta:
continue
current_updated = t_meta.get("updated")
# 1. New output from subagent
if current_updated and current_updated != last_wm:
if last_wm is None:
# Seed initial watermark so we only trigger on subsequent turns
subagent_tracker.update_session(sid, last_watermark=current_updated, last_activity_at=utcnow())
continue
history, _ = muse_hybrid.get_history(agent, thread_id=sid, limit=2)
snippet = ""
if history:
for m in reversed(history):
if m.get("role") == "assistant":
snippet = m.get("text", "").strip()[:180]
break
subagent_tracker.update_session(
sid,
last_watermark=current_updated,
last_activity_at=utcnow(),
timeout_warned=False
)
short_id = sid[:8]
alert_text = f"[SUBAGENT-UPDATE] Subagent '{title}' ({short_id}) has posted new output."
if snippet:
alert_text += f"\nSnippet: {snippet}..."
alert_text += f"\nRun `box thread view {sid}` to review and aggregate deliverables."
alerts.append(alert_text)
# 2. 15-minute stall watchdog
elif not s.get("timeout_warned"):
last_act = s.get("last_activity_at") or s.get("spawned_at")
if last_act:
try:
dt = datetime.fromisoformat(last_act.replace("Z", "+00:00"))
idle_sec = (now - dt).total_seconds()
if idle_sec >= 900: # 15 minutes
subagent_tracker.update_session(sid, timeout_warned=True)
short_id = sid[:8]
alerts.append(
f"[WARN] Subagent '{title}' ({short_id}) has had no activity for 15+ minutes. "
f"Check status with `box thread view {sid}` or consider respawning."
)
except Exception:
pass
except Exception as e:
log("%s: subagent check error: %s" % (agent, e))
return alerts
def do_check(only_agent=None):
st = load_state()
cfg = get_config(st)
@@ -417,14 +496,22 @@ def do_check(only_agent=None):
threads_meta, _ = muse_hybrid.get_threads(agent)
sc_activity = check_sidechats(agent, cfg, wm, threads_meta)
# 3. Subagent reactive monitoring check
subagent_alerts = check_subagents(agent, cfg, threads_meta)
agents_state[agent] = wm
total_new = len(main_new) + sum(len(a["messages"]) for a in sc_activity)
total_new = len(main_new) + sum(len(a["messages"]) for a in sc_activity) + len(subagent_alerts)
if total_new == 0:
results[agent] = {"ok": True, "new": 0}
continue
digest = compose_dual_digest(agent, main_new, sc_activity)
parts = []
if len(main_new) > 0 or len(sc_activity) > 0:
parts.append(compose_dual_digest(agent, main_new, sc_activity))
if subagent_alerts:
parts.extend(subagent_alerts)
digest = "\n\n".join(parts)[:DIGEST_MAX * 2]
sidechat = cfg["prompt_sidechat"].get(agent)
if not sidechat:
log("%s: no prompting sidechat configured, skipping" % agent)