feat(mux): implement onboarding prompt initialization, 4-tier UUID validation, and TUI 1:1 cross-matching
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
#
|
||||
# --dry-run: 부수효과 없는 read-only. "지금 뭐 돌고 있지?" 질문에 안전.
|
||||
# multi-agent-mux-status 스킬이 이걸 재사용.
|
||||
# 어떤 분기로도 디스크/DB 쓰기가 발생하지 않음을 보장함.
|
||||
#
|
||||
# 출력 (JSON): {timestamp, yaml_path, herdr_sessions_alive, herdr_confirmed, drifts, actions}
|
||||
#
|
||||
@@ -39,7 +40,15 @@ while [ $# -gt 0 ]; do
|
||||
--subscribe) SUBSCRIBE=1; shift ;;
|
||||
--timeout) SUB_TIMEOUT="$2"; shift 2 ;;
|
||||
--idle-timeout) SUB_IDLE_TIMEOUT="$2"; shift 2 ;;
|
||||
-h|--help) echo "Usage: $0 [--once] [--emit-diff] [--dry-run] [--subscribe [--timeout N] [--idle-timeout N]]"; exit 0 ;;
|
||||
-h|--help)
|
||||
cat <<EOF
|
||||
Usage: $0 [--once] [--emit-diff] [--dry-run] [--subscribe [--timeout N] [--idle-timeout N]]
|
||||
|
||||
Options:
|
||||
--dry-run Runs in read-only mode, guaranteeing no database or file writes are performed.
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*) echo "ERROR: unknown arg: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
@@ -306,6 +315,8 @@ import os, json, glob, subprocess, time, sqlite3
|
||||
from datetime import datetime, timezone
|
||||
import yaml
|
||||
|
||||
exec(os.environ['MAM_VERIFY_PY'])
|
||||
|
||||
yaml_path = os.environ['YAML_PATH']
|
||||
home = os.environ['HOME_DIR']
|
||||
claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects")
|
||||
@@ -381,6 +392,31 @@ def pane_meta(session, srv):
|
||||
return None
|
||||
|
||||
|
||||
def _pin_and_verify_resume(s, agent, cwd, uuid, degraded=False):
|
||||
own_key = {
|
||||
'claude': 'claude_session_id_own',
|
||||
'agy': 'agy_conversation_id_own',
|
||||
'hermes': 'hermes_conversation_id_own',
|
||||
'cline': 'cline_conversation_id_own'
|
||||
}[agent]
|
||||
s[own_key] = uuid
|
||||
s['last_visible_status'] = 'pinned'
|
||||
resume_cmd = ['bash', os.path.join(skills_dir, 'multi-agent-mux-resume', 'scripts', 'resume_session.sh'),
|
||||
'--workspace', cwd, '--agent', agent, '--session', s['name'], '--dry-run']
|
||||
res = subprocess.run(resume_cmd, capture_output=True, text=True)
|
||||
if res.returncode == 0:
|
||||
s['last_visible_status'] = 'resume_verified'
|
||||
else:
|
||||
s['last_visible_status'] = f"resume dry-run failed: {res.stderr.strip() or res.stdout.strip()}"
|
||||
|
||||
id_name = 'session' if agent in ('claude', 'cline') else 'conversation'
|
||||
if not degraded:
|
||||
drifts.append({'class': 'C', 'name': s['name'], 'msg': f"{s['name']}: {id_name} id materialized: {uuid}"})
|
||||
else:
|
||||
drifts.append({'class': 'C-degraded', 'name': s['name'], 'msg': f"{s['name']}: TUI viewport check unavailable, pinned via stage 1-3 only"})
|
||||
actions.append(f"updated {id_name} id: {uuid}")
|
||||
|
||||
|
||||
yaml_sessions = d.get('herdr_sessions', [])
|
||||
yaml_session_names = {s['name'] for s in yaml_sessions if s.get('name')}
|
||||
alive_set = {(t['name'], t.get('server', 'default')) for t in herdr_sessions}
|
||||
@@ -492,29 +528,29 @@ for s in d.get('herdr_sessions', []):
|
||||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
if not cwd:
|
||||
continue
|
||||
proj_key = cwd.replace('/', '-').replace('_', '-')
|
||||
proj_dir = f"{claude_project_dir}/{proj_key}"
|
||||
key = workspace_key(cwd)
|
||||
iso = s.get('isolation', {}).get('root') if isinstance(s.get('isolation'), dict) else None
|
||||
proj_dir = f"{iso}/projects/{key}" if iso else f"{claude_project_dir}/{key}"
|
||||
if not os.path.isdir(proj_dir):
|
||||
continue
|
||||
jsonls = sorted(glob.glob(f"{proj_dir}/*.jsonl"), key=os.path.getmtime, reverse=True)
|
||||
if not jsonls:
|
||||
continue
|
||||
latest = jsonls[0]
|
||||
if time.time() - os.path.getmtime(latest) > 300:
|
||||
continue
|
||||
try:
|
||||
with open(latest) as f:
|
||||
first = f.readline().strip()
|
||||
if not first:
|
||||
continue
|
||||
sid = json.loads(first).get('sessionId')
|
||||
if not sid:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
s['claude_session_id_own'] = sid
|
||||
drifts.append({'class': 'C', 'name': s['name'], 'msg': f"{s['name']}: session id materialized: {sid}"})
|
||||
actions.append(f"updated session id: {sid}")
|
||||
|
||||
valid_candidates = []
|
||||
for latest in jsonls:
|
||||
uuid = os.path.basename(latest)[:-6]
|
||||
if verify_session_uuid(cwd, 'claude', uuid, s, mode="discover"):
|
||||
valid_candidates.append(uuid)
|
||||
|
||||
if len(valid_candidates) == 1:
|
||||
uuid = valid_candidates[0]
|
||||
cmd = ['bash', '-c', f'source "{lib_sh}" && verify_tui_viewport "{s["name"]}" "claude" "{cwd}"']
|
||||
rc = subprocess.run(cmd).returncode
|
||||
if rc == 0:
|
||||
_pin_and_verify_resume(s, 'claude', cwd, uuid, degraded=False)
|
||||
elif rc == 1:
|
||||
drifts.append({'class': 'C-warn', 'name': s['name'], 'msg': f"{s['name']}: TUI viewport mismatch for candidate {uuid} — not pinned, will retry next cycle"})
|
||||
else:
|
||||
_pin_and_verify_resume(s, 'claude', cwd, uuid, degraded=True)
|
||||
|
||||
# === drift C (agy): agy 새 session id materialize (per-row own id) ===
|
||||
for s in d.get('herdr_sessions', []):
|
||||
@@ -527,18 +563,28 @@ for s in d.get('herdr_sessions', []):
|
||||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
if not cwd:
|
||||
continue
|
||||
lc = f"{home}/.gemini/antigravity-cli/cache/last_conversations.json"
|
||||
if os.path.exists(lc):
|
||||
try:
|
||||
with open(lc) as f:
|
||||
lc_data = json.load(f)
|
||||
cid = lc_data.get(cwd)
|
||||
if cid and os.path.exists(f"{home}/.gemini/antigravity-cli/conversations/{cid}.db"):
|
||||
s['agy_conversation_id_own'] = cid
|
||||
drifts.append({'class': 'C', 'name': s['name'], 'msg': f"{s['name']}: conversation id materialized: {cid}"})
|
||||
actions.append(f"updated conversation id: {cid}")
|
||||
except Exception:
|
||||
pass
|
||||
iso = s.get('isolation', {}).get('root') if isinstance(s.get('isolation'), dict) else None
|
||||
conv_dir = f"{iso}/.gemini/antigravity-cli/conversations" if iso else f"{home}/.gemini/antigravity-cli/conversations"
|
||||
if not os.path.isdir(conv_dir):
|
||||
continue
|
||||
dbs = sorted(glob.glob(f"{conv_dir}/*.db"), key=os.path.getmtime, reverse=True)
|
||||
|
||||
valid_candidates = []
|
||||
for db in dbs:
|
||||
uuid = os.path.basename(db)[:-3]
|
||||
if verify_session_uuid(cwd, 'agy', uuid, s, mode="discover"):
|
||||
valid_candidates.append(uuid)
|
||||
|
||||
if len(valid_candidates) == 1:
|
||||
uuid = valid_candidates[0]
|
||||
cmd = ['bash', '-c', f'source "{lib_sh}" && verify_tui_viewport "{s["name"]}" "agy" "{cwd}"']
|
||||
rc = subprocess.run(cmd).returncode
|
||||
if rc == 0:
|
||||
_pin_and_verify_resume(s, 'agy', cwd, uuid, degraded=False)
|
||||
elif rc == 1:
|
||||
drifts.append({'class': 'C-warn', 'name': s['name'], 'msg': f"{s['name']}: TUI viewport mismatch for candidate {uuid} — not pinned, will retry next cycle"})
|
||||
else:
|
||||
_pin_and_verify_resume(s, 'agy', cwd, uuid, degraded=True)
|
||||
|
||||
# === drift C (hermes): hermes 새 session id materialize (per-row own id) ===
|
||||
for s in d.get('herdr_sessions', []):
|
||||
@@ -551,19 +597,33 @@ for s in d.get('herdr_sessions', []):
|
||||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
if not cwd:
|
||||
continue
|
||||
hdb = f"{home}/.hermes/state.db"
|
||||
if os.path.exists(hdb):
|
||||
try:
|
||||
conn = sqlite3.connect(hdb)
|
||||
r = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 1", (cwd,)).fetchone()
|
||||
conn.close()
|
||||
if r:
|
||||
cid = r[0]
|
||||
s['hermes_conversation_id_own'] = cid
|
||||
drifts.append({'class': 'C', 'name': s['name'], 'msg': f"{s['name']}: conversation id materialized: {cid}"})
|
||||
actions.append(f"updated conversation id: {cid}")
|
||||
except Exception:
|
||||
pass
|
||||
iso = s.get('isolation', {}).get('root') if isinstance(s.get('isolation'), dict) else None
|
||||
hdb = f"{iso}/.hermes/state.db" if iso else f"{home}/.hermes/state.db"
|
||||
if not os.path.exists(hdb):
|
||||
continue
|
||||
|
||||
valid_candidates = []
|
||||
try:
|
||||
conn = sqlite3.connect(hdb)
|
||||
r = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 1", (cwd,)).fetchone()
|
||||
conn.close()
|
||||
if r:
|
||||
uuid = r[0]
|
||||
if verify_session_uuid(cwd, 'hermes', uuid, s, mode="discover"):
|
||||
valid_candidates.append(uuid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(valid_candidates) == 1:
|
||||
uuid = valid_candidates[0]
|
||||
cmd = ['bash', '-c', f'source "{lib_sh}" && verify_tui_viewport "{s["name"]}" "hermes" "{cwd}"']
|
||||
rc = subprocess.run(cmd).returncode
|
||||
if rc == 0:
|
||||
_pin_and_verify_resume(s, 'hermes', cwd, uuid, degraded=False)
|
||||
elif rc == 1:
|
||||
drifts.append({'class': 'C-warn', 'name': s['name'], 'msg': f"{s['name']}: TUI viewport mismatch for candidate {uuid} — not pinned, will retry next cycle"})
|
||||
else:
|
||||
_pin_and_verify_resume(s, 'hermes', cwd, uuid, degraded=True)
|
||||
|
||||
# === drift C (cline): cline 새 session id materialize (per-row own id) ===
|
||||
for s in d.get('herdr_sessions', []):
|
||||
@@ -576,29 +636,36 @@ for s in d.get('herdr_sessions', []):
|
||||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
if not cwd:
|
||||
continue
|
||||
sessions_dir = f"{home}/.cline/data/sessions"
|
||||
if os.path.isdir(sessions_dir):
|
||||
candidates = []
|
||||
for session_folder in glob.glob(f"{sessions_dir}/*"):
|
||||
if os.path.isdir(session_folder):
|
||||
folder_name = os.path.basename(session_folder)
|
||||
json_file = f"{session_folder}/{folder_name}.json"
|
||||
if os.path.exists(json_file):
|
||||
candidates.append(json_file)
|
||||
candidates.sort(key=os.path.getmtime, reverse=True)
|
||||
for j in candidates:
|
||||
try:
|
||||
with open(j) as f:
|
||||
sdata = json.load(f)
|
||||
if sdata.get('cwd') == cwd or sdata.get('workspace_root') == cwd:
|
||||
cid = sdata.get('session_id')
|
||||
if cid:
|
||||
s['cline_conversation_id_own'] = cid
|
||||
drifts.append({'class': 'C', 'name': s['name'], 'msg': f"{s['name']}: session id materialized: {cid}"})
|
||||
actions.append(f"updated session id: {cid}")
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
iso = s.get('isolation', {}).get('root') if isinstance(s.get('isolation'), dict) else None
|
||||
sessions_dir = f"{iso}/sessions" if iso else f"{home}/.cline/data/sessions"
|
||||
if not os.path.isdir(sessions_dir):
|
||||
continue
|
||||
candidates = []
|
||||
for session_folder in glob.glob(f"{sessions_dir}/*"):
|
||||
if os.path.isdir(session_folder):
|
||||
folder_name = os.path.basename(session_folder)
|
||||
json_file = f"{session_folder}/{folder_name}.json"
|
||||
if os.path.exists(json_file):
|
||||
candidates.append(json_file)
|
||||
candidates.sort(key=os.path.getmtime, reverse=True)
|
||||
|
||||
valid_candidates = []
|
||||
for j in candidates:
|
||||
uuid = os.path.basename(j)[:-5]
|
||||
if verify_session_uuid(cwd, 'cline', uuid, s, mode="discover"):
|
||||
valid_candidates.append(uuid)
|
||||
break
|
||||
|
||||
if len(valid_candidates) == 1:
|
||||
uuid = valid_candidates[0]
|
||||
cmd = ['bash', '-c', f'source "{lib_sh}" && verify_tui_viewport "{s["name"]}" "cline" "{cwd}"']
|
||||
rc = subprocess.run(cmd).returncode
|
||||
if rc == 0:
|
||||
_pin_and_verify_resume(s, 'cline', cwd, uuid, degraded=False)
|
||||
elif rc == 1:
|
||||
drifts.append({'class': 'C-warn', 'name': s['name'], 'msg': f"{s['name']}: TUI viewport mismatch for candidate {uuid} — not pinned, will retry next cycle"})
|
||||
else:
|
||||
_pin_and_verify_resume(s, 'cline', cwd, uuid, degraded=True)
|
||||
|
||||
# === drift D: stale UUID (cache 의 artifact 가 사라짐) — 보고만, 변경 없음 ===
|
||||
ai = d.get('agent_identities', {}) or {}
|
||||
@@ -653,7 +720,7 @@ if not actions:
|
||||
PYEOF
|
||||
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
printf '%s' "$RECON_SRC" | LIB_SH="$LIB_SH" env_python "$AGENT_SESSIONS_YAML"
|
||||
printf '%s' "$RECON_SRC" | LIB_SH="$LIB_SH" MAM_VERIFY_PY="$VERIFY_SESSION_PYTHON" env_python "$AGENT_SESSIONS_YAML"
|
||||
else
|
||||
printf '%s' "$RECON_SRC" | LIB_SH="$LIB_SH" atomic_dump_yaml "$AGENT_SESSIONS_YAML"
|
||||
printf '%s' "$RECON_SRC" | LIB_SH="$LIB_SH" MAM_VERIFY_PY="$VERIFY_SESSION_PYTHON" atomic_dump_yaml "$AGENT_SESSIONS_YAML"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user