refactor(isolation): simplify agent session isolation and remove legacy home-isolation helpers

This commit is contained in:
2026-07-23 22:05:29 +09:00
parent 15ffc8f6bb
commit f0a2103edf
7 changed files with 91 additions and 347 deletions
+3 -5
View File
@@ -53,10 +53,9 @@ $ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \
--agent claude \ --agent claude \
--role developer \ --role developer \
--session my-project-dev-claude \ --session my-project-dev-claude \
--isolate \ --herdr-server multi-agent-mux
--herdr-workspace multi-agent-mux
``` ```
* `--isolate` 옵션을 주면 `.mam/agent_homes/<uuid>/` 하위에 로그인 및 설정은 유지하되 대화 내역은 격리되는 홈이 형성됩니다. * 모든 세션은 사용자의 전역 에이전트 설정(Global Config)을 공유하며, herdr 프로세스 격리 및 대화 UUID 단위로 독립 구동됩니다.
### 2) 세션 접속 (Attach) ### 2) 세션 접속 (Attach)
백그라운드에서 구동된 에이전트 TUI 화면에 들어갑니다. 백그라운드에서 구동된 에이전트 TUI 화면에 들어갑니다.
@@ -66,7 +65,7 @@ $ herdr session attach my-project-dev-claude
* **화면 탈출**: 대화 중 세션을 유지한 채 터미널로 돌아오려면 `Ctrl + B`를 누른 뒤 `D` 키를 차례로 입력합니다. * **화면 탈출**: 대화 중 세션을 유지한 채 터미널로 돌아오려면 `Ctrl + B`를 누른 뒤 `D` 키를 차례로 입력합니다.
### 3) 에이전트 상태 복원 (Resume) ### 3) 에이전트 상태 복원 (Resume)
세션이 중지되었거나, 호스트 재기동으로 tmux가 소멸한 경우에도 이전 대화 ID 및 격리 디렉토리를 원자적으로 이어받아 다시 기동할 수 있습니다. 세션이 중지되었거나, 호스트 재기동으로 tmux가 소멸한 경우에도 이전 대화 ID를 원자적으로 이어받아 다시 기동할 수 있습니다.
```bash ```bash
# 1단계: 복원 대상 세션의 UUID 자동 조회 (DB/YAML 레지스트리 기반) # 1단계: 복원 대상 세션의 UUID 자동 조회 (DB/YAML 레지스트리 기반)
$ WORKSPACE="/path/to/your/project" $ WORKSPACE="/path/to/your/project"
@@ -80,7 +79,6 @@ $ UUID=$(bash .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.s
$ [ -n "$UUID" ] || { echo "[ERROR] 매칭되는 활성 세션 이력이 없습니다. create_session.sh를 통해 먼저 세션을 생성해 주세요."; exit 1; } $ [ -n "$UUID" ] || { echo "[ERROR] 매칭되는 활성 세션 이력이 없습니다. create_session.sh를 통해 먼저 세션을 생성해 주세요."; exit 1; }
# 2단계: 세션 재기동 (이전 대화 컨텍스트 복원 기동) # 2단계: 세션 재기동 (이전 대화 컨텍스트 복원 기동)
# (주의: 만약 create 시 격리(--isolate) 세션으로 생성했다면, CLAUDE_CONFIG_DIR 환경변수를 YAML에 기록된 isolation.root 경로로 지정하여 띄워야 합니다. 상세 격리 복원 커맨드는 .agents/skills/multi-agent-mux-resume/SKILL.md 문서를 필독해 주세요.)
$ herdr run -d --name "$SESSION_NAME" --workspace "$WORKSPACE" -- \ $ herdr run -d --name "$SESSION_NAME" --workspace "$WORKSPACE" -- \
"claude --dangerously-skip-permissions -r $UUID" "claude --dangerously-skip-permissions -r $UUID"
+33 -158
View File
@@ -546,7 +546,11 @@ for s in d.get('herdr_sessions', []):
if s.get('name') == name: if s.get('name') == name:
print(s.get('herdr_workspace') or s.get('herdr_server') or 'default') print(s.get('herdr_workspace') or s.get('herdr_server') or 'default')
sys.exit(0) sys.exit(0)
print(os.environ.get('HERDR_SERVER_NAME', 'default')) fallback = os.environ.get('HERDR_SERVER_NAME', '')
if not fallback or fallback == 'default':
pwd = os.path.abspath(os.getcwd())
fallback = os.path.basename(pwd)
print(fallback or 'default')
" "
} }
@@ -890,13 +894,12 @@ def verify_session_uuid(ws, agent, uuid, row=None, home_dir=None, claude_dir=Non
row = row or {} row = row or {}
epoch = row.get("herdr_session_epoch", 0) epoch = row.get("herdr_session_epoch", 0)
cwd = row.get("pane", {}).get("cwd", "") or ws cwd = row.get("pane", {}).get("cwd", "") or ws
iso = row.get("isolation", {}).get("root") if isinstance(row.get("isolation"), dict) else None
if workspace_key(cwd) != workspace_key(ws): if workspace_key(cwd) != workspace_key(ws):
return False return False
if agent == "claude": if agent == "claude":
base = f"{iso}/projects" if iso else c_dir base = c_dir
key = workspace_key(ws) key = workspace_key(ws)
path = f"{base}/{key}/{uuid}.jsonl" path = f"{base}/{key}/{uuid}.jsonl"
if not os.path.exists(path): if not os.path.exists(path):
@@ -917,21 +920,24 @@ def verify_session_uuid(ws, agent, uuid, row=None, home_dir=None, claude_dir=Non
return False return False
elif agent == "agy": elif agent == "agy":
base = f"{iso}/.gemini/antigravity-cli/conversations" if iso else f"{home}/.gemini/antigravity-cli/conversations" base = f"{home}/.gemini/antigravity-cli/conversations"
path = f"{base}/{uuid}.db" path = f"{base}/{uuid}.db"
if not os.path.exists(path): if not os.path.exists(path):
return False return False
if epoch and os.path.getmtime(path) < epoch: if epoch and os.path.getmtime(path) < epoch:
return False return False
if mode == "discover": if mode == "discover":
lc = f"{iso}/.gemini/antigravity-cli/cache/last_conversations.json" if iso else f"{home}/.gemini/antigravity-cli/cache/last_conversations.json" lc = f"{home}/.gemini/antigravity-cli/cache/last_conversations.json"
cache_match = False
if os.path.exists(lc): if os.path.exists(lc):
try: try:
with open(lc) as f: with open(lc) as f:
lc_data = json.load(f) lc_data = json.load(f)
if lc_data.get(cwd) != uuid: cache_match = (lc_data.get(cwd) == uuid)
return False
except Exception: except Exception:
cache_match = False
if not cache_match:
if uuid in (row.get("_sibling_claimed_uuids") or []):
return False return False
try: try:
conn = sqlite3.connect(path) conn = sqlite3.connect(path)
@@ -943,7 +949,7 @@ def verify_session_uuid(ws, agent, uuid, row=None, home_dir=None, claude_dir=Non
return False return False
elif agent == "hermes": elif agent == "hermes":
hdb = f"{iso}/.hermes/state.db" if iso else f"{home}/.hermes/state.db" hdb = f"{home}/.hermes/state.db"
if not os.path.exists(hdb): if not os.path.exists(hdb):
return False return False
if epoch and os.path.getmtime(hdb) < epoch: if epoch and os.path.getmtime(hdb) < epoch:
@@ -958,7 +964,7 @@ def verify_session_uuid(ws, agent, uuid, row=None, home_dir=None, claude_dir=Non
return False return False
elif agent == "cline": elif agent == "cline":
base = f"{iso}/sessions" if iso else f"{home}/.cline/data/sessions" base = f"{home}/.cline/data/sessions"
path = f"{base}/{uuid}/{uuid}.json" path = f"{base}/{uuid}/{uuid}.json"
if not os.path.exists(path): if not os.path.exists(path):
return False return False
@@ -1265,169 +1271,34 @@ capture_conversation_id() {
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Session isolation (all-L2) — implementation_plan.session_isolation.md Rev.3 # Session isolation — Universal Global Config (Option A, Job 536a6625)
# #
# Every isolated session gets its own state home `.mam/agent_homes/<uuid>/`; # Config-home isolation (.mam/agent_homes/<uuid>/) was removed in favor of:
# auth/config files are SYMLINKED into it (never copied — token refresh must # 1. Universal Global Config: all agents read/write standard ~/.claude, ~/.gemini,
# converge on the real files). Levers per agent (Phase 0 measured matrix): # ~/.hermes, ~/.cline user configuration and credential stores.
# claude: CLAUDE_CONFIG_DIR=<root> conv: <root>/projects/<key>/<uuid>.jsonl # 2. Process Isolation: each agent-workspace pair runs in its own herdr pane.
# cline: --data-dir <root> conv: <root>/sessions/<id>/<id>.json # 3. Conversation Isolation: session UUIDs discriminate conversation history.
# agy: HOME=<root> conv: <root>/.gemini/antigravity-cli/conversations/
# hermes: HOME=<root> conv: <root>/.hermes/state.db
# #
# provision_isolation <agent> <root> — mkdir + seed; prints comma-joined seeded list # Stubbed isolation functions kept for backward compatibility:
# isolation_lever <agent> — claude_config_dir|cline_data_dir|home
# isolation_env_prefix <agent> <root> — "VAR=<root> " spawn prefix ('' for cline)
# isolation_cmd_args <agent> <root> — extra spawn CLI args ('' unless cline)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
provision_isolation() { provision_isolation() {
local agent="$1" root="$2" seeded="" f base local agent="$1" root="$2"
mkdir -p "$root" printf ''
case "$agent" in
claude)
if [ -e "$HOME/.claude.json" ]; then ln -sfn "$HOME/.claude.json" "$root/.claude.json"; seeded=".claude.json"; fi
if [ -e "$HOME/.claude/.credentials.json" ]; then ln -sfn "$HOME/.claude/.credentials.json" "$root/.credentials.json"; seeded="${seeded:+$seeded,}.credentials.json"; fi
if [ -e "$HOME/.claude/settings.json" ]; then
if [ ! -e "$root/settings.json" ]; then cp -a "$HOME/.claude/settings.json" "$root/settings.json"; fi
seeded="${seeded:+$seeded,}settings.json"
fi
if [ -d "$HOME/.claude/plugins" ]; then ln -sfn "$HOME/.claude/plugins" "$root/plugins"; seeded="${seeded:+$seeded,}plugins"; fi
if [ -d "$HOME/.claude/session-env" ]; then ln -sfn "$HOME/.claude/session-env" "$root/session-env"; seeded="${seeded:+$seeded,}session-env"; fi
if [ -d "$HOME/.claude/sessions" ]; then ln -sfn "$HOME/.claude/sessions" "$root/sessions"; seeded="${seeded:+$seeded,}sessions"; fi
if [ -d "$HOME/.claude/cache" ]; then ln -sfn "$HOME/.claude/cache" "$root/cache"; seeded="${seeded:+$seeded,}cache"; fi
;;
cline)
# CLI 가 --data-dir <root> 를 읽을 때 최상위 루트 하위에서 설정을 찾으므로 다이렉트 맵핑
mkdir -p "$root/settings"
for f in "$HOME/.cline/data/settings/"*; do
[ -e "$f" ] || continue
base="$(basename "$f")"
ln -sfn "$f" "$root/settings/$base"; seeded="${seeded:+$seeded,}settings/$base"
done
if [ -e "$HOME/.cline/data/globalState.json" ]; then
ln -sfn "$HOME/.cline/data/globalState.json" "$root/globalState.json"; seeded="${seeded:+$seeded,}globalState.json"
fi
# 인증과 DB 바인딩을 연계하여 Welcome Screen 튕김 방지
mkdir -p "$root/db"
for f in "$HOME/.cline/data/db/"*; do
[ -e "$f" ] || continue
base="$(basename "$f")"
# DB 락 경쟁 크래시 방지를 위해 물리 복사(cp -p)로 직접 주입
cp -p "$f" "$root/db/$base"
seeded="${seeded:+$seeded,}db/$base"
done
;;
agy)
mkdir -p "$root/.gemini/antigravity-cli"
for f in oauth_creds.json google_accounts.json installation_id settings.json state.json; do
if [ -e "$HOME/.gemini/$f" ]; then ln -sfn "$HOME/.gemini/$f" "$root/.gemini/$f"; seeded="${seeded:+$seeded,}.gemini/$f"; fi
done
for f in antigravity-oauth-token installation_id settings.json conversation_summaries.db jetski_state.pbtxt; do
if [ -e "$HOME/.gemini/antigravity-cli/$f" ]; then ln -sfn "$HOME/.gemini/antigravity-cli/$f" "$root/.gemini/antigravity-cli/$f"; seeded="${seeded:+$seeded,}.gemini/antigravity-cli/$f"; fi
done
if [ -d "$HOME/.gemini/antigravity" ]; then
ln -sfn "$HOME/.gemini/antigravity" "$root/.gemini/antigravity"
seeded="${seeded:+$seeded,}.gemini/antigravity"
fi
if [ -d "$HOME/.gemini/antigravity-ide" ]; then
if [ ! -d "$root/.gemini/antigravity-ide" ]; then cp -a "$HOME/.gemini/antigravity-ide" "$root/.gemini/antigravity-ide"; fi
seeded="${seeded:+$seeded,}.gemini/antigravity-ide"
fi
if [ -d "$HOME/.gemini/config" ]; then
ln -sfn "$HOME/.gemini/config" "$root/.gemini/config"
seeded="${seeded:+$seeded,}.gemini/config"
fi
# On macOS, seed ~/Library/Keychains to allow isolated agy to query Keychain Access credentials
# Also seed Preferences and Application Support for Antigravity settings/TOS/Theme (using cp -a for write isolation)
if [ "$(uname)" = "Darwin" ]; then
mkdir -p "$root/Library"
if [ -d "$HOME/Library/Keychains" ]; then
ln -sfn "$HOME/Library/Keychains" "$root/Library/Keychains"
seeded="${seeded:+$seeded,}Library/Keychains"
fi
mkdir -p "$root/Library/Preferences"
for plist in com.google.antigravity.plist com.google.antigravity-ide.plist com.google.GeminiMacOS.plist com.google.GeminiMacOS.shareddata.plist com.google.GeminiMacOS.launcher.plist; do
if [ -f "$HOME/Library/Preferences/$plist" ]; then
if [ ! -f "$root/Library/Preferences/$plist" ]; then cp -a "$HOME/Library/Preferences/$plist" "$root/Library/Preferences/$plist"; fi
seeded="${seeded:+$seeded,}Library/Preferences/$plist"
fi
done
mkdir -p "$root/Library/Application Support"
if [ -d "$HOME/Library/Application Support/Antigravity" ]; then
if [ ! -d "$root/Library/Application Support/Antigravity" ]; then cp -a "$HOME/Library/Application Support/Antigravity" "$root/Library/Application Support/Antigravity"; fi
seeded="${seeded:+$seeded,}Library/Application Support/Antigravity"
fi
if [ -d "$HOME/Library/Application Support/Antigravity IDE" ]; then
if [ ! -d "$root/Library/Application Support/Antigravity IDE" ]; then cp -a "$HOME/Library/Application Support/Antigravity IDE" "$root/Library/Application Support/Antigravity IDE"; fi
seeded="${seeded:+$seeded,}Library/Application Support/Antigravity IDE"
fi
if [ -d "$HOME/Library/Application Support/com.google.GeminiMacOS" ]; then
if [ ! -d "$root/Library/Application Support/com.google.GeminiMacOS" ]; then cp -a "$HOME/Library/Application Support/com.google.GeminiMacOS" "$root/Library/Application Support/com.google.GeminiMacOS"; fi
seeded="${seeded:+$seeded,}Library/Application Support/com.google.GeminiMacOS"
fi
if [ -d "$HOME/Library/Group Containers/group.com.google.gemini" ]; then
mkdir -p "$root/Library/Group Containers"
ln -sfn "$HOME/Library/Group Containers/group.com.google.gemini" "$root/Library/Group Containers/group.com.google.gemini"
seeded="${seeded:+$seeded,}Library/Group Containers/group.com.google.gemini"
fi
else
# On Linux/Unix, seed XDG config and local data dirs for Antigravity settings/TOS (using cp -a for write isolation)
local xdg_config="${XDG_CONFIG_HOME:-$HOME/.config}"
local xdg_data="${XDG_DATA_HOME:-$HOME/.local/share}"
if [ -d "$xdg_config/Antigravity" ]; then
mkdir -p "$root/.config"
if [ ! -d "$root/.config/Antigravity" ]; then cp -a "$xdg_config/Antigravity" "$root/.config/Antigravity"; fi
seeded="${seeded:+$seeded,}.config/Antigravity"
elif [ -d "$xdg_config/antigravity" ]; then
mkdir -p "$root/.config"
if [ ! -d "$root/.config/antigravity" ]; then cp -a "$xdg_config/antigravity" "$root/.config/antigravity"; fi
seeded="${seeded:+$seeded,}.config/antigravity"
fi
if [ -d "$xdg_data/Antigravity" ]; then
mkdir -p "$root/.local/share"
if [ ! -d "$root/.local/share/Antigravity" ]; then cp -a "$xdg_data/Antigravity" "$root/.local/share/Antigravity"; fi
seeded="${seeded:+$seeded,}.local/share/Antigravity"
elif [ -d "$xdg_data/antigravity" ]; then
mkdir -p "$root/.local/share"
if [ ! -d "$root/.local/share/antigravity" ]; then cp -a "$xdg_data/antigravity" "$root/.local/share/antigravity"; fi
seeded="${seeded:+$seeded,}.local/share/antigravity"
fi
fi
;;
hermes)
mkdir -p "$root/.hermes"
for f in auth.json config.yaml .env; do
if [ -e "$HOME/.hermes/$f" ]; then ln -sfn "$HOME/.hermes/$f" "$root/.hermes/$f"; seeded="${seeded:+$seeded,}.hermes/$f"; fi
done
;;
esac
printf '%s\n' "$seeded"
} }
isolation_lever() { isolation_lever() {
case "$1" in case "$1" in
claude) echo "claude_config_dir" ;; claude|agy|hermes|cline) echo "none" ;;
cline) echo "cline_data_dir" ;; *) echo "" ;;
agy|hermes) echo "home" ;;
*) echo "" ;;
esac esac
} }
isolation_env_prefix() { isolation_env_prefix() {
local agent="$1" root="$2" :
case "$agent" in
claude) printf 'CLAUDE_CONFIG_DIR=%q ' "$root" ;;
agy|hermes) printf 'HOME=%q ' "$root" ;;
*) : ;;
esac
} }
isolation_cmd_args() { isolation_cmd_args() {
local agent="$1" root="$2" :
case "$agent" in
cline) printf -- '--data-dir %q' "$root" ;;
*) : ;;
esac
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -1596,11 +1467,15 @@ wait_for_tui_ready() {
for i in {1..30}; do for i in {1..30}; do
if _pane_dialog_open "$sess"; then if _pane_dialog_open "$sess"; then
if printf '%s\n' "$(_pane_tail "$sess" 5)" | grep -q 'Press Enter to continue'; then
_sks_herdr send-keys -t "$sess" Enter || true
sleep 1
fi
sleep 1 sleep 1
continue continue
fi fi
local content local content
content=$($local_herdr capture-pane -p -t "$sess" 2>/dev/null || echo "") content=$(_sks_herdr capture-pane -p -t "$sess" 2>/dev/null || echo "")
if [ -n "$content" ]; then if [ -n "$content" ]; then
case "$agent" in case "$agent" in
claude) claude)
@@ -36,8 +36,8 @@ Options:
--submit-job PROMPT submit a job to multi-agent-mux-delegate-job registry with the given prompt --submit-job PROMPT submit a job to multi-agent-mux-delegate-job registry with the given prompt
--onboard automatically submit a project alignment/orientation job to the new agent --onboard automatically submit a project alignment/orientation job to the new agent
--no-onboard disable automatic onboarding job submission --no-onboard disable automatic onboarding job submission
--no-isolate disable state isolation (shares global configuration/history) --no-isolate legacy flag (no-op; all sessions use global configuration)
[default: isolated mode is always active] --isolate legacy flag (no-op; all sessions use global configuration)
-h, --help this help -h, --help this help
EOF EOF
} }
@@ -65,8 +65,8 @@ while [ $# -gt 0 ]; do
--submit-job) SUBMIT_JOB_PROMPT="$2"; shift 2 ;; --submit-job) SUBMIT_JOB_PROMPT="$2"; shift 2 ;;
--onboard) ONBOARD=1; shift ;; --onboard) ONBOARD=1; shift ;;
--no-onboard) ONBOARD=0; shift ;; --no-onboard) ONBOARD=0; shift ;;
--isolate) ISOLATE=1; shift ;; # legacy compatibility --isolate) echo "NOTE: --isolate/--no-isolate is a no-op — config-home isolation was removed; sessions always use global config." >&2; shift ;; # legacy compatibility
--no-isolate) ISOLATE=0; shift ;; --no-isolate) echo "NOTE: --isolate/--no-isolate is a no-op — config-home isolation was removed; sessions always use global config." >&2; shift ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
*) echo "ERROR: unknown arg: $1" >&2; usage; exit 2 ;; *) echo "ERROR: unknown arg: $1" >&2; usage; exit 2 ;;
esac esac
@@ -121,37 +121,14 @@ if _herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
exit 3 exit 3
fi fi
# T3: 세션 격리 프로비저닝 (all-L2) — 격리 홈 생성 + auth/설정 심링크 시딩 # Config-home isolation was removed in favor of global config + process/UUID isolation.
# (implementation_plan.session_isolation.md Rev.3 / Phase 0 실측 매트릭스 기준)
ISOLATION_UUID=""
ISOLATION_ROOT=""
ISOLATION_LEVER=""
ISOLATION_SEEDED=""
if [ "$ISOLATE" = "1" ]; then
command -v uuidgen >/dev/null || { echo "ERROR: uuidgen not found (required for --isolate)" >&2; exit 1; }
WORKSPACE_ABS="$(cd "$WORKSPACE" && pwd)"
ISOLATION_UUID="$(uuidgen)"
ISOLATION_ROOT="$WORKSPACE_ABS/.mam/agent_homes/$ISOLATION_UUID"
ISOLATION_LEVER="$(isolation_lever "$AGENT")"
if [ "$DRY_RUN" = "1" ]; then
echo "[dry-run] would provision isolation: lever=$ISOLATION_LEVER root=$ISOLATION_ROOT"
else
ISOLATION_SEEDED="$(provision_isolation "$AGENT" "$ISOLATION_ROOT")"
echo "isolation: lever=$ISOLATION_LEVER root=$ISOLATION_ROOT"
fi
fi
# herdr 세션 띄우기 # herdr 세션 띄우기
LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}" LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}"
WRAPPER="$LOCAL_BIN/$SESSION_NAME" WRAPPER="$LOCAL_BIN/$SESSION_NAME"
# cmd_full 결정 — T4: isolation 디스패치(env prefix / CLI args)를 시작 명령에 주입. if [ -z "${HERDR_SERVER_NAME:-}" ] || [ "$HERDR_SERVER_NAME" = "default" ]; then
# 격리 미사용 시 기존 문자열과 byte-identical (V4 회귀 0). export HERDR_SERVER_NAME="multi-agent-mux"
ISO_ENV_PREFIX=""
ISO_CMD_ARGS=""
if [ -n "$ISOLATION_ROOT" ]; then
ISO_ENV_PREFIX="$(isolation_env_prefix "$AGENT" "$ISOLATION_ROOT")"
ISO_CMD_ARGS="$(isolation_cmd_args "$AGENT" "$ISOLATION_ROOT")"
fi fi
# Resolve absolute path of the agent command to prevent herdr PATH inheritance issues (especially on macOS) # Resolve absolute path of the agent command to prevent herdr PATH inheritance issues (especially on macOS)
@@ -161,7 +138,9 @@ if [ "$AGENT" = "cline" ]; then
RESOLVED_BIN="$(command -v cline)" RESOLVED_BIN="$(command -v cline)"
fi fi
else else
if command -v "$AGENT" >/dev/null 2>&1; then if [ "$AGENT" = "claude" ] && [ -x "$HOME/.nvm/versions/node/v24.15.0/bin/claude" ]; then
RESOLVED_BIN="$HOME/.nvm/versions/node/v24.15.0/bin/claude"
elif command -v "$AGENT" >/dev/null 2>&1; then
RESOLVED_BIN="$(command -v "$AGENT")" RESOLVED_BIN="$(command -v "$AGENT")"
fi fi
fi fi
@@ -172,25 +151,27 @@ if [ "$(uname)" = "Darwin" ] && [ -f "$RESOLVED_BIN" ]; then
fi fi
case "$AGENT" in case "$AGENT" in
claude) CMD_FULL="${ISO_ENV_PREFIX}${RESOLVED_BIN} --dangerously-skip-permissions" ;; claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;;
agy) CMD_FULL="${ISO_ENV_PREFIX}${RESOLVED_BIN} --dangerously-skip-permissions" ;; agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;;
hermes) CMD_FULL="${ISO_ENV_PREFIX}${RESOLVED_BIN}" ;; hermes) CMD_FULL="${RESOLVED_BIN}" ;;
cline) CMD_FULL="${RESOLVED_BIN} -i${ISO_CMD_ARGS:+ $ISO_CMD_ARGS}" ;; cline) CMD_FULL="${RESOLVED_BIN} -i" ;;
esac esac
spawn() { spawn() {
if [ -z "${HERDR_SERVER_NAME:-}" ] || [ "$HERDR_SERVER_NAME" = "default" ]; then
export HERDR_SERVER_NAME="multi-agent-mux"
fi
case "$AGENT" in case "$AGENT" in
claude) claude)
# 격리 시 wrapper 경로는 env 주입을 운반하지 못하므로 인라인 spawn 강제 if { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$USE_WRAPPER" = "1" ]; then
if [ "$ISOLATE" != "1" ] && { { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$USE_WRAPPER" = "1" ]; }; then
nohup "$WRAPPER" >/dev/null 2>&1 & nohup "$WRAPPER" >/dev/null 2>&1 &
disown disown
else else
_herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL" HERDR_SERVER_NAME="$HERDR_SERVER_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
fi fi
;; ;;
agy|hermes|cline) agy|hermes|cline)
_herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL" HERDR_SERVER_NAME="$HERDR_SERVER_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
;; ;;
*) echo "ERROR: --agent must be claude, agy, hermes or cline, got: $AGENT" >&2; exit 2 ;; *) echo "ERROR: --agent must be claude, agy, hermes or cline, got: $AGENT" >&2; exit 2 ;;
esac esac
@@ -209,21 +190,20 @@ cleanup_herdr_on_error() {
if [ $exit_code -ne 0 ]; then if [ $exit_code -ne 0 ]; then
echo "⚠️ Error occurred during initialization. Rolling back and killing herdr session '$SESSION_NAME'..." >&2 echo "⚠️ Error occurred during initialization. Rolling back and killing herdr session '$SESSION_NAME'..." >&2
_herdr kill-session -t "$SESSION_NAME" 2>/dev/null || true _herdr kill-session -t "$SESSION_NAME" 2>/dev/null || true
# T3 rollback: 이 세션용으로 프로비저닝한 격리 홈 제거 (경로 가드 후 rm)
if [ -n "$ISOLATION_ROOT" ] && [ -d "$ISOLATION_ROOT" ]; then
case "$ISOLATION_ROOT" in
*/.mam/agent_homes/*) rm -rf "$ISOLATION_ROOT" ;;
esac
fi
fi fi
} }
trap cleanup_herdr_on_error EXIT trap cleanup_herdr_on_error EXIT
export HERDR_SERVER_NAME="${HERDR_SERVER_NAME:-$(resolve_herdr_server "$SESSION_NAME")}"
# TUI 준비 대기 # TUI 준비 대기
if ! wait_for_tui_ready "$SESSION_NAME" "$AGENT"; then if ! wait_for_tui_ready "$SESSION_NAME" "$AGENT"; then
echo "ERROR: agent TUI never became ready — aborting (rollback via trap)" >&2 echo "ERROR: agent TUI never became ready — aborting (rollback via trap)" >&2
exit 1 exit 1
fi fi
if [ "$AGENT" = "claude" ]; then
handle_startup_dialogs "$SESSION_NAME" 15
fi
# pane 메타 캡처 # pane 메타 캡처
PANE_PID=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null || echo "") PANE_PID=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null || echo "")
@@ -232,11 +212,9 @@ PANE_CMD=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_current_command}' 2>/
HERDR_EPOCH=$(date +%s) HERDR_EPOCH=$(date +%s)
NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ') NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
# 시작 명령 (CMD_FULL 은 spawn 전에 isolation 디스패치를 반영해 확정됨 — T4) # 시작 명령
# NOTE: this must match what `spawn()` actually ran above — env-var-driven # NOTE: this must match what `spawn()` actually ran above — env-var-driven
# isolation (HERDR_SERVER_NAME picked up by the lib.sh shim), not a # herdr server shim (HERDR_SERVER_NAME picked up by the lib.sh shim).
# `--workspace <label>` flag (real `herdr agent start --workspace` wants an
# actual workspace id like "w2", which HERDR_SERVER_NAME is not).
START_CMD="HERDR_SERVER_NAME=${HERDR_SERVER_NAME:-default} herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\"" START_CMD="HERDR_SERVER_NAME=${HERDR_SERVER_NAME:-default} herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
# If --onboard is specified, automatically build the onboarding prompt # If --onboard is specified, automatically build the onboarding prompt
@@ -285,9 +263,7 @@ atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
HERDR_EPOCH="$HERDR_EPOCH" PANE_PID="$PANE_PID" PANE_CWD="$PANE_CWD" \ HERDR_EPOCH="$HERDR_EPOCH" PANE_PID="$PANE_PID" PANE_CWD="$PANE_CWD" \
CMD_FULL="$CMD_FULL" START_CMD="$START_CMD" CHILD_PID="$CHILD_PID" \ CMD_FULL="$CMD_FULL" START_CMD="$START_CMD" CHILD_PID="$CHILD_PID" \
HERDR_SERVER_NAME="${HERDR_SERVER_NAME:-default}" \ HERDR_SERVER_NAME="${HERDR_SERVER_NAME:-default}" \
DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" \ DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" <<'PYEOF'
ISOLATION_UUID="$ISOLATION_UUID" ISOLATION_ROOT="$ISOLATION_ROOT" \
ISOLATION_LEVER="$ISOLATION_LEVER" ISOLATION_SEEDED="$ISOLATION_SEEDED" <<'PYEOF'
name = os.environ['SESSION_NAME'] name = os.environ['SESSION_NAME']
agent = os.environ['AGENT'] agent = os.environ['AGENT']
role = os.environ['ROLE'] role = os.environ['ROLE']
@@ -331,15 +307,7 @@ entry = {
'kill_command': f'HERDR_SERVER_NAME={server_name} herdr kill-session -t {name}', 'kill_command': f'HERDR_SERVER_NAME={server_name} herdr kill-session -t {name}',
} }
# T5: isolation 블록 영속화 (all-L2) — resume/resolve/stop 이 재적용의 단일 소스로 사용
iso_uuid = os.environ.get('ISOLATION_UUID', '')
if iso_uuid:
entry['isolation'] = {
'uuid': iso_uuid,
'root': os.environ.get('ISOLATION_ROOT', ''),
'lever': os.environ.get('ISOLATION_LEVER', ''),
'seeded': [x for x in os.environ.get('ISOLATION_SEEDED', '').split(',') if x],
}
if agent == 'claude': if agent == 'claude':
entry['tui'] = { entry['tui'] = {
@@ -319,6 +319,7 @@ exec(os.environ['MAM_VERIFY_PY'])
yaml_path = os.environ['YAML_PATH'] yaml_path = os.environ['YAML_PATH']
home = os.environ['HOME_DIR'] home = os.environ['HOME_DIR']
skills_dir = os.environ.get('SKILLS_DIR', '')
claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects") claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects")
now_iso = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') now_iso = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
@@ -529,8 +530,7 @@ for s in d.get('herdr_sessions', []):
if not cwd: if not cwd:
continue continue
key = workspace_key(cwd) key = workspace_key(cwd)
iso = s.get('isolation', {}).get('root') if isinstance(s.get('isolation'), dict) else None proj_dir = f"{claude_project_dir}/{key}"
proj_dir = f"{iso}/projects/{key}" if iso else f"{claude_project_dir}/{key}"
if not os.path.isdir(proj_dir): if not os.path.isdir(proj_dir):
continue continue
jsonls = sorted(glob.glob(f"{proj_dir}/*.jsonl"), key=os.path.getmtime, reverse=True) jsonls = sorted(glob.glob(f"{proj_dir}/*.jsonl"), key=os.path.getmtime, reverse=True)
@@ -563,16 +563,26 @@ for s in d.get('herdr_sessions', []):
cwd = (s.get('pane') or {}).get('cwd', '') cwd = (s.get('pane') or {}).get('cwd', '')
if not cwd: if not cwd:
continue continue
iso = s.get('isolation', {}).get('root') if isinstance(s.get('isolation'), dict) else None conv_dir = f"{home}/.gemini/antigravity-cli/conversations"
conv_dir = f"{iso}/.gemini/antigravity-cli/conversations" if iso else f"{home}/.gemini/antigravity-cli/conversations"
if not os.path.isdir(conv_dir): if not os.path.isdir(conv_dir):
continue continue
dbs = sorted(glob.glob(f"{conv_dir}/*.db"), key=os.path.getmtime, reverse=True) dbs = sorted(glob.glob(f"{conv_dir}/*.db"), key=os.path.getmtime, reverse=True)
sibling_claimed = [
other.get('agy_conversation_id_own')
for other in d.get('herdr_sessions', [])
if other is not s
and (other.get('pane') or {}).get('cwd') == cwd
and other.get('status') not in ('stopped', 'terminated')
and other.get('agy_conversation_id_own')
]
s_eval = dict(s)
s_eval['_sibling_claimed_uuids'] = sibling_claimed
valid_candidates = [] valid_candidates = []
for db in dbs: for db in dbs:
uuid = os.path.basename(db)[:-3] uuid = os.path.basename(db)[:-3]
if verify_session_uuid(cwd, 'agy', uuid, s, mode="discover"): if verify_session_uuid(cwd, 'agy', uuid, s_eval, mode="discover"):
valid_candidates.append(uuid) valid_candidates.append(uuid)
if len(valid_candidates) == 1: if len(valid_candidates) == 1:
@@ -597,8 +607,7 @@ for s in d.get('herdr_sessions', []):
cwd = (s.get('pane') or {}).get('cwd', '') cwd = (s.get('pane') or {}).get('cwd', '')
if not cwd: if not cwd:
continue continue
iso = s.get('isolation', {}).get('root') if isinstance(s.get('isolation'), dict) else None hdb = f"{home}/.hermes/state.db"
hdb = f"{iso}/.hermes/state.db" if iso else f"{home}/.hermes/state.db"
if not os.path.exists(hdb): if not os.path.exists(hdb):
continue continue
@@ -636,8 +645,7 @@ for s in d.get('herdr_sessions', []):
cwd = (s.get('pane') or {}).get('cwd', '') cwd = (s.get('pane') or {}).get('cwd', '')
if not cwd: if not cwd:
continue continue
iso = s.get('isolation', {}).get('root') if isinstance(s.get('isolation'), dict) else None sessions_dir = f"{home}/.cline/data/sessions"
sessions_dir = f"{iso}/sessions" if iso else f"{home}/.cline/data/sessions"
if not os.path.isdir(sessions_dir): if not os.path.isdir(sessions_dir):
continue continue
candidates = [] candidates = []
+4 -54
View File
@@ -64,7 +64,7 @@ WORKSPACE=/path/to/project
AGENT=claude # or agy or hermes AGENT=claude # or agy or hermes
SESSION_NAME=<workspace>-creator-<agent> # same convention as multi-agent-mux-create SESSION_NAME=<workspace>-creator-<agent> # same convention as multi-agent-mux-create
# Resolve the isolated herdr server name & load isolation utils # Resolve the isolated herdr server name & load common utils
source .agents/skills/lib.sh source .agents/skills/lib.sh
# 1. Resolve the session id (T5: pass session name for target-row isolation check) # 1. Resolve the session id (T5: pass session name for target-row isolation check)
@@ -84,45 +84,7 @@ if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
exec herdr agent attach "$SESSION_NAME" exec herdr agent attach "$SESSION_NAME"
fi fi
# 3. Resolve isolation settings for this session (T4/T5 re-apply) # 3. Determine CMD_FULL
# _get_session_isolation resolves isolation block for the session row
ISO_ROOT=""
ISO_ENV=""
ISO_ARGS=""
ISO_DATA=$(env_python "$AGENT_SESSIONS_YAML" SESSION_NAME="$SESSION_NAME" <<'PYEOF'
import os, json, yaml, sqlite3
name = os.environ['SESSION_NAME']
yaml_path = os.environ['YAML_PATH']
db_path = os.path.splitext(yaml_path)[0] + '.db'
d = {}
try:
if os.path.exists(db_path):
conn = sqlite3.connect(db_path, timeout=60.0)
row = conn.execute('SELECT data FROM sessions WHERE name=?', (name,)).fetchone()
if row:
s = json.loads(row[0])
print(json.dumps(s.get('isolation') or {}))
raise SystemExit(0)
elif os.path.exists(yaml_path):
with open(yaml_path) as f:
d = yaml.safe_load(f) or {}
except Exception:
pass
for s in d.get('herdr_sessions', []):
if s.get('name') == name:
print(json.dumps(s.get('isolation') or {}))
raise SystemExit(0)
print("{}")
PYEOF
)
ISO_ROOT=$(printf '%s' "$ISO_DATA" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("root",""))')
if [ -n "$ISO_ROOT" ]; then
ISO_ENV="$(isolation_env_prefix "$AGENT" "$ISO_ROOT")"
ISO_ARGS="$(isolation_cmd_args "$AGENT" "$ISO_ROOT")"
echo "Re-applying isolation: root=$ISO_ROOT env=$ISO_ENV args=$ISO_ARGS"
fi
# Determine CMD_FULL with isolation applied
case "$AGENT" in case "$AGENT" in
claude) CMD_FULL="claude --dangerously-skip-permissions -r $UUID" ;; claude) CMD_FULL="claude --dangerously-skip-permissions -r $UUID" ;;
agy) CMD_FULL="agy --dangerously-skip-permissions --conversation $UUID" ;; agy) CMD_FULL="agy --dangerously-skip-permissions --conversation $UUID" ;;
@@ -130,22 +92,10 @@ case "$AGENT" in
cline) CMD_FULL="cline -i --id $UUID" ;; cline) CMD_FULL="cline -i --id $UUID" ;;
esac esac
# Prepend env prefix and append command args (T4) # 4. Spawn new herdr session + run agent with the saved id
if [ -n "$ISO_ENV" ]; then
CMD_FULL="$ISO_ENV $CMD_FULL"
fi
if [ -n "$ISO_ARGS" ]; then
CMD_FULL="$CMD_FULL $ISO_ARGS"
fi
# 4. Spawn new herdr session + run agent with the saved id (and re-applied isolation)
case "$AGENT" in case "$AGENT" in
claude) claude)
if [ -z "$ISO_ROOT" ] && [ -x "$HOME/.local/bin/canary-projects-multi-agent-mux-creator-claude" ]; then START_CMD="herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
START_CMD="herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$HOME/.local/bin/canary-projects-multi-agent-mux-creator-claude\""
else
START_CMD="herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
fi
eval "$START_CMD" eval "$START_CMD"
# auto-handle trust / bypass dialogs # auto-handle trust / bypass dialogs
handle_startup_dialogs "$SESSION_NAME" 20 handle_startup_dialogs "$SESSION_NAME" 20
@@ -9,7 +9,7 @@ usage() {
Usage: $0 --workspace <path> --agent <claude|agy|hermes|cline> --session <name> [--dry-run] Usage: $0 --workspace <path> --agent <claude|agy|hermes|cline> --session <name> [--dry-run]
Options: Options:
--dry-run Simulates resume flow (resolves binary, environment, isolation) without writing --dry-run Simulates resume flow (resolves binary, environment) without writing
any updates to YAML or DB. Safe to execute inside active write transactions. any updates to YAML or DB. Safe to execute inside active write transactions.
EOF EOF
} }
@@ -60,45 +60,6 @@ if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
exit 0 exit 0
fi fi
# 3. Resolve isolation settings for this session
ISO_ROOT=""
ISO_ENV=""
ISO_ARGS=""
ISO_DATA=$(env_python "$AGENT_SESSIONS_YAML" SESSION_NAME="$SESSION_NAME" <<'PYEOF'
import os, json, yaml, sqlite3
name = os.environ['SESSION_NAME']
yaml_path = os.environ['YAML_PATH']
db_path = os.path.splitext(yaml_path)[0] + '.db'
d = {}
try:
if os.path.exists(db_path):
conn = sqlite3.connect(db_path, timeout=60.0)
row = conn.execute('SELECT data FROM sessions WHERE name=?', (name,)).fetchone()
if row:
s = json.loads(row[0])
print(json.dumps(s.get('isolation') or {}))
raise SystemExit(0)
elif os.path.exists(yaml_path):
with open(yaml_path) as f:
d = yaml.safe_load(f) or {}
except Exception:
pass
for s in d.get('herdr_sessions', []):
if s.get('name') == name:
print(json.dumps(s.get('isolation') or {}))
raise SystemExit(0)
print("{}")
PYEOF
)
ISO_ROOT=$(printf '%s' "$ISO_DATA" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("root",""))')
if [ -n "$ISO_ROOT" ]; then
ISO_ENV="$(isolation_env_prefix "$AGENT" "$ISO_ROOT")"
ISO_ARGS="$(isolation_cmd_args "$AGENT" "$ISO_ROOT")"
echo "Re-applying isolation: root=$ISO_ROOT env=$ISO_ENV args=$ISO_ARGS"
fi
# Resolve absolute path of the agent command to prevent herdr PATH inheritance issues (especially on macOS) # Resolve absolute path of the agent command to prevent herdr PATH inheritance issues (especially on macOS)
RESOLVED_BIN="$AGENT" RESOLVED_BIN="$AGENT"
if [ "$AGENT" = "cline" ]; then if [ "$AGENT" = "cline" ]; then
@@ -116,7 +77,7 @@ if [ "$(uname)" = "Darwin" ] && [ -f "$RESOLVED_BIN" ]; then
xattr -d com.apple.quarantine "$RESOLVED_BIN" 2>/dev/null || true xattr -d com.apple.quarantine "$RESOLVED_BIN" 2>/dev/null || true
fi fi
# Determine CMD_FULL with isolation applied # Determine CMD_FULL
case "$AGENT" in case "$AGENT" in
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions -r $UUID" ;; claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions -r $UUID" ;;
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --conversation $UUID" ;; agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --conversation $UUID" ;;
@@ -125,20 +86,6 @@ case "$AGENT" in
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;; *) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
esac esac
# Prepend env prefix and append command args (T4)
if [ -n "$ISO_ENV" ]; then
CMD_FULL="$ISO_ENV $CMD_FULL"
fi
if [ -n "$ISO_ARGS" ]; then
CMD_FULL="$CMD_FULL $ISO_ARGS"
fi
# Validate isolation root if it was configured
if [ -n "$ISO_ROOT" ] && [ ! -d "$ISO_ROOT" ]; then
echo "ERROR: Isolation root directory does not exist: $ISO_ROOT" >&2
exit 1
fi
# Validate binary exists and is executable # Validate binary exists and is executable
if [ -f "$RESOLVED_BIN" ] || [[ "$RESOLVED_BIN" == /* ]] || [[ "$RESOLVED_BIN" == ~/* ]]; then if [ -f "$RESOLVED_BIN" ] || [[ "$RESOLVED_BIN" == /* ]] || [[ "$RESOLVED_BIN" == ~/* ]]; then
if [ ! -x "$RESOLVED_BIN" ]; then if [ ! -x "$RESOLVED_BIN" ]; then
+3 -5
View File
@@ -53,10 +53,9 @@ $ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \
--agent claude \ --agent claude \
--role developer \ --role developer \
--session my-project-dev-claude \ --session my-project-dev-claude \
--isolate \ --herdr-server multi-agent-mux
--herdr-workspace multi-agent-mux
``` ```
* `--isolate` 옵션을 주면 `.mam/agent_homes/<uuid>/` 하위에 로그인 및 설정은 유지하되 대화 내역은 격리되는 홈이 형성됩니다. * 모든 세션은 사용자의 전역 에이전트 설정(Global Config)을 공유하며, herdr 프로세스 격리 및 대화 UUID 단위로 독립 구동됩니다.
### 2) 세션 접속 (Attach) ### 2) 세션 접속 (Attach)
백그라운드에서 구동된 에이전트 TUI 화면에 들어갑니다. 백그라운드에서 구동된 에이전트 TUI 화면에 들어갑니다.
@@ -66,7 +65,7 @@ $ herdr session attach my-project-dev-claude
* **화면 탈출**: 대화 중 세션을 유지한 채 터미널로 돌아오려면 `Ctrl + B`를 누른 뒤 `D` 키를 차례로 입력합니다. * **화면 탈출**: 대화 중 세션을 유지한 채 터미널로 돌아오려면 `Ctrl + B`를 누른 뒤 `D` 키를 차례로 입력합니다.
### 3) 에이전트 상태 복원 (Resume) ### 3) 에이전트 상태 복원 (Resume)
세션이 중지되었거나, 호스트 재기동으로 herdr 서버가 소멸한 경우에도 이전 대화 ID 및 격리 디렉토리를 원자적으로 이어받아 다시 기동할 수 있습니다. 세션이 중지되었거나, 호스트 재기동으로 herdr 서버가 소멸한 경우에도 이전 대화 ID를 원자적으로 이어받아 다시 기동할 수 있습니다.
```bash ```bash
# 1단계: 복원 대상 세션의 UUID 자동 조회 (DB/YAML 레지스트리 기반) # 1단계: 복원 대상 세션의 UUID 자동 조회 (DB/YAML 레지스트리 기반)
$ WORKSPACE="/path/to/your/project" $ WORKSPACE="/path/to/your/project"
@@ -80,7 +79,6 @@ $ UUID=$(bash .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.s
$ [ -n "$UUID" ] || { echo "[ERROR] 매칭되는 활성 세션 이력이 없습니다. create_session.sh를 통해 먼저 세션을 생성해 주세요."; exit 1; } $ [ -n "$UUID" ] || { echo "[ERROR] 매칭되는 활성 세션 이력이 없습니다. create_session.sh를 통해 먼저 세션을 생성해 주세요."; exit 1; }
# 2단계: 세션 재기동 (이전 대화 컨텍스트 복원 기동) # 2단계: 세션 재기동 (이전 대화 컨텍스트 복원 기동)
# (주의: 만약 create 시 격리(--isolate) 세션으로 생성했다면, CLAUDE_CONFIG_DIR 환경변수를 YAML에 기록된 isolation.root 경로로 지정하여 띄워야 합니다. 상세 격리 복원 커맨드는 .agents/skills/multi-agent-mux-resume/SKILL.md 문서를 필독해 주세요.)
$ herdr run -d --name "$SESSION_NAME" --workspace "$WORKSPACE" -- \ $ herdr run -d --name "$SESSION_NAME" --workspace "$WORKSPACE" -- \
"claude --dangerously-skip-permissions -r $UUID" "claude --dangerously-skip-permissions -r $UUID"