#!/usr/bin/env bash # update_yaml_resumed.sh — multi-agent-mux-resume 의 부속 스크립트 # Resume 한 세션의 agent-sessions.yaml 엔트리를 status=running + resume 메타로 갱신. # resume UUID 를 per-row own id (claude_session_id_own / agy_conversation_id_own) # 에 박는다 — agent_identities 전역은 더 이상 primary 아님 (cache 로 강등, P0-C/단계 e). # # Usage: bash update_yaml_resumed.sh --session --uuid [--agent claude|agy] set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh" usage() { cat < --uuid [--agent claude|agy] EOF } SESSION_NAME="" UUID="" AGENT="" WORKSPACE="" ROLE="" while [ $# -gt 0 ]; do case "$1" in --session) SESSION_NAME="$2"; shift 2 ;; --uuid) UUID="$2"; shift 2 ;; --agent) AGENT="$2"; shift 2 ;; --workspace) WORKSPACE="$2"; shift 2 ;; --role) ROLE="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "ERROR: unknown arg: $1" >&2; exit 2 ;; esac done [ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; exit 2; } [ -n "$UUID" ] || { echo "ERROR: --uuid required" >&2; exit 2; } [ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; } HERDR_SESSION_NAME="$(resolve_herdr_session "$SESSION_NAME" "${WORKSPACE:-}")" export HERDR_SESSION_NAME # --agent 미지정 시 이름 suffix 로 fallback (P1-F: 가능하면 --agent 명시) if [ -z "$AGENT" ]; then case "$SESSION_NAME" in *-creator-claude|*-planner-claude|*-reviewer-claude) AGENT=claude ;; *-creator-agy|*-planner-agy|*-reviewer-agy) AGENT=agy ;; *-creator-hermes|*-planner-hermes|*-reviewer-hermes) AGENT=hermes ;; *-creator-cline|*-planner-cline|*-reviewer-cline) AGENT=cline ;; *) echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2; exit 2 ;; esac fi if [ -z "$ROLE" ]; then case "$SESSION_NAME" in *-planner-*) ROLE="planner" ;; *-reviewer-*) ROLE="reviewer" ;; *) ROLE="creator" ;; esac fi NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ') NOW_EPOCH=$(date +%s) # 새 herdr pane pid / 자식 pid 를 bash 에서 캡처 (env 로 전달, P1-B) PANE_PID=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null | head -1 || true) PANE_PID="${PANE_PID:-}" CHILD_PID=0 if { [ "$AGENT" = "agy" ] || [ "$AGENT" = "hermes" ] || [ "$AGENT" = "cline" ]; } && [ -n "$PANE_PID" ]; then CHILD_PID=$(pgrep -P "$PANE_PID" -x "$AGENT" 2>/dev/null | head -1 || true) CHILD_PID="${CHILD_PID:-0}" fi DELEGATE_JOB_ID=$(MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$SESSION_NAME" python3 -c " import sys, os, json name = os.environ['SESSION_NAME'] d = json.loads(os.environ.get('MAM_STATE_JSON', '{}')) for s in d.get('herdr_sessions', []): if s.get('name') == name: print(s.get('delegate_job_id', '') or '') sys.exit(0) ") atomic_dump_yaml "$AGENT_SESSIONS_YAML" \ SESSION_NAME="$SESSION_NAME" UUID="$UUID" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \ NOW_EPOCH="$NOW_EPOCH" TARGET_WORKSPACE="${WORKSPACE:-$WORKSPACE_ROOT}" ROLE="$ROLE" \ PANE_PID="$PANE_PID" CHILD_PID="$CHILD_PID" <<'PYEOF' name = os.environ['SESSION_NAME'] uuid = os.environ['UUID'] agent = os.environ['AGENT'] now = os.environ['NOW_ISO'] epoch = int(os.environ.get('NOW_EPOCH', '0') or '0') ws_root = os.environ.get('TARGET_WORKSPACE', '') or os.environ.get('WORKSPACE_ROOT', '') role = os.environ.get('ROLE', 'creator') pane_pid = os.environ.get('PANE_PID', '') target = None for s in d.get('herdr_sessions', []): if s.get('name') == name: target = s break if target is None: pwd = os.path.abspath(ws_root) default_server = 'mam-' + os.path.basename(pwd).lower().replace('_', '-') server_name = os.environ.get('HERDR_SESSION_NAME', default_server) target = { 'name': name, 'status': 'running', 'role': role, 'herdr_session_created_at': now, 'herdr_session_epoch': epoch, 'herdr_session': server_name, 'delegate_job_id': None, 'pane': {'index': 0, 'pid': int(pane_pid) if pane_pid.isdigit() else 0, 'cmd': agent, 'cwd': ws_root}, 'start_command': f'HERDR_SESSION_NAME={server_name} herdr agent attach {name}', 'attach_command': f'HERDR_SESSION_NAME={server_name} herdr agent attach {name}', 'kill_command': f'HERDR_SESSION_NAME={server_name} herdr kill-session -t {name}', } d.setdefault('herdr_sessions', []).append(target) target['status'] = 'running' target.pop('terminated_at', None) target.pop('terminated_at_epoch', None) target.pop('termination_mode', None) target.pop('archived_at', None) # stop 메타도 정리 — resume 하면 더 이상 stopped 상태가 아니므로 잔존 필드를 제거. target.pop('stopped_at', None) target.pop('stopped_at_epoch', None) target.pop('stop_reason', None) target.pop('resumable', None) target['last_visible_status'] = f'resumed conversation {uuid} at {now}' target.setdefault('pane', {}) if pane_pid.isdigit(): target['pane']['pid'] = int(pane_pid) if agent == 'claude': target['pane']['cmd'] = 'claude' target['pane']['cmd_full'] = f'claude --dangerously-skip-permissions -r {uuid}' target['claude_session_id_own'] = uuid elif agent == 'agy': target['pane']['cmd'] = 'agy' target['pane']['cmd_full'] = f'agy --dangerously-skip-permissions --conversation {uuid}' target['agy_conversation_id_own'] = uuid cp = os.environ.get('CHILD_PID', '0') if cp.isdigit() and int(cp) > 0: target['child_pid'] = int(cp) elif agent == 'hermes': target['pane']['cmd'] = 'hermes' target['pane']['cmd_full'] = f'hermes --resume {uuid}' target['hermes_conversation_id_own'] = uuid cp = os.environ.get('CHILD_PID', '0') if cp.isdigit() and int(cp) > 0: target['child_pid'] = int(cp) elif agent == 'cline': target['pane']['cmd'] = 'cline' target['pane']['cmd_full'] = f'cline -i --id {uuid}' target['cline_conversation_id_own'] = uuid cp = os.environ.get('CHILD_PID', '0') if cp.isdigit() and int(cp) > 0: target['child_pid'] = int(cp) snap = d.setdefault('snapshot', {}) snap['taken_at'] = now snap.pop('terminated_at', None) snap.pop('terminated_at_epoch', None) print(f"updated: {name} status=running (resume id -> per-row own id)", flush=True) PYEOF delegate_publish_event "$DELEGATE_JOB_ID" progress "resumed"