refactor(isolation): simplify agent session isolation and remove legacy home-isolation helpers
This commit is contained in:
+3
-5
@@ -53,10 +53,9 @@ $ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \
|
||||
--agent claude \
|
||||
--role developer \
|
||||
--session my-project-dev-claude \
|
||||
--isolate \
|
||||
--herdr-workspace multi-agent-mux
|
||||
--herdr-server multi-agent-mux
|
||||
```
|
||||
* `--isolate` 옵션을 주면 `.mam/agent_homes/<uuid>/` 하위에 로그인 및 설정은 유지하되 대화 내역은 격리되는 홈이 형성됩니다.
|
||||
* 모든 세션은 사용자의 전역 에이전트 설정(Global Config)을 공유하며, herdr 프로세스 격리 및 대화 UUID 단위로 독립 구동됩니다.
|
||||
|
||||
### 2) 세션 접속 (Attach)
|
||||
백그라운드에서 구동된 에이전트 TUI 화면에 들어갑니다.
|
||||
@@ -66,7 +65,7 @@ $ herdr session attach my-project-dev-claude
|
||||
* **화면 탈출**: 대화 중 세션을 유지한 채 터미널로 돌아오려면 `Ctrl + B`를 누른 뒤 `D` 키를 차례로 입력합니다.
|
||||
|
||||
### 3) 에이전트 상태 복원 (Resume)
|
||||
세션이 중지되었거나, 호스트 재기동으로 tmux가 소멸한 경우에도 이전 대화 ID 및 격리 디렉토리를 원자적으로 이어받아 다시 기동할 수 있습니다.
|
||||
세션이 중지되었거나, 호스트 재기동으로 tmux가 소멸한 경우에도 이전 대화 ID를 원자적으로 이어받아 다시 기동할 수 있습니다.
|
||||
```bash
|
||||
# 1단계: 복원 대상 세션의 UUID 자동 조회 (DB/YAML 레지스트리 기반)
|
||||
$ 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; }
|
||||
|
||||
# 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" -- \
|
||||
"claude --dangerously-skip-permissions -r $UUID"
|
||||
|
||||
|
||||
+32
-157
@@ -546,7 +546,11 @@ for s in d.get('herdr_sessions', []):
|
||||
if s.get('name') == name:
|
||||
print(s.get('herdr_workspace') or s.get('herdr_server') or 'default')
|
||||
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 {}
|
||||
epoch = row.get("herdr_session_epoch", 0)
|
||||
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):
|
||||
return False
|
||||
|
||||
if agent == "claude":
|
||||
base = f"{iso}/projects" if iso else c_dir
|
||||
base = c_dir
|
||||
key = workspace_key(ws)
|
||||
path = f"{base}/{key}/{uuid}.jsonl"
|
||||
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
|
||||
|
||||
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"
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
if epoch and os.path.getmtime(path) < epoch:
|
||||
return False
|
||||
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):
|
||||
try:
|
||||
with open(lc) as f:
|
||||
lc_data = json.load(f)
|
||||
if lc_data.get(cwd) != uuid:
|
||||
return False
|
||||
cache_match = (lc_data.get(cwd) == uuid)
|
||||
except Exception:
|
||||
cache_match = False
|
||||
if not cache_match:
|
||||
if uuid in (row.get("_sibling_claimed_uuids") or []):
|
||||
return False
|
||||
try:
|
||||
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
|
||||
|
||||
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):
|
||||
return False
|
||||
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
|
||||
|
||||
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"
|
||||
if not os.path.exists(path):
|
||||
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>/`;
|
||||
# auth/config files are SYMLINKED into it (never copied — token refresh must
|
||||
# converge on the real files). Levers per agent (Phase 0 measured matrix):
|
||||
# claude: CLAUDE_CONFIG_DIR=<root> conv: <root>/projects/<key>/<uuid>.jsonl
|
||||
# cline: --data-dir <root> conv: <root>/sessions/<id>/<id>.json
|
||||
# agy: HOME=<root> conv: <root>/.gemini/antigravity-cli/conversations/
|
||||
# hermes: HOME=<root> conv: <root>/.hermes/state.db
|
||||
# Config-home isolation (.mam/agent_homes/<uuid>/) was removed in favor of:
|
||||
# 1. Universal Global Config: all agents read/write standard ~/.claude, ~/.gemini,
|
||||
# ~/.hermes, ~/.cline user configuration and credential stores.
|
||||
# 2. Process Isolation: each agent-workspace pair runs in its own herdr pane.
|
||||
# 3. Conversation Isolation: session UUIDs discriminate conversation history.
|
||||
#
|
||||
# provision_isolation <agent> <root> — mkdir + seed; prints comma-joined seeded list
|
||||
# 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)
|
||||
# Stubbed isolation functions kept for backward compatibility:
|
||||
# ---------------------------------------------------------------------------
|
||||
provision_isolation() {
|
||||
local agent="$1" root="$2" seeded="" f base
|
||||
mkdir -p "$root"
|
||||
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"
|
||||
local agent="$1" root="$2"
|
||||
printf ''
|
||||
}
|
||||
|
||||
isolation_lever() {
|
||||
case "$1" in
|
||||
claude) echo "claude_config_dir" ;;
|
||||
cline) echo "cline_data_dir" ;;
|
||||
agy|hermes) echo "home" ;;
|
||||
claude|agy|hermes|cline) echo "none" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
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() {
|
||||
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
|
||||
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
|
||||
continue
|
||||
fi
|
||||
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
|
||||
case "$agent" in
|
||||
claude)
|
||||
|
||||
@@ -36,8 +36,8 @@ Options:
|
||||
--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
|
||||
--no-onboard disable automatic onboarding job submission
|
||||
--no-isolate disable state isolation (shares global configuration/history)
|
||||
[default: isolated mode is always active]
|
||||
--no-isolate legacy flag (no-op; all sessions use global configuration)
|
||||
--isolate legacy flag (no-op; all sessions use global configuration)
|
||||
-h, --help this help
|
||||
EOF
|
||||
}
|
||||
@@ -65,8 +65,8 @@ while [ $# -gt 0 ]; do
|
||||
--submit-job) SUBMIT_JOB_PROMPT="$2"; shift 2 ;;
|
||||
--onboard) ONBOARD=1; shift ;;
|
||||
--no-onboard) ONBOARD=0; shift ;;
|
||||
--isolate) ISOLATE=1; shift ;; # legacy compatibility
|
||||
--no-isolate) ISOLATE=0; shift ;;
|
||||
--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) 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 ;;
|
||||
*) echo "ERROR: unknown arg: $1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
@@ -121,37 +121,14 @@ if _herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# T3: 세션 격리 프로비저닝 (all-L2) — 격리 홈 생성 + auth/설정 심링크 시딩
|
||||
# (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
|
||||
# Config-home isolation was removed in favor of global config + process/UUID isolation.
|
||||
|
||||
# herdr 세션 띄우기
|
||||
LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}"
|
||||
WRAPPER="$LOCAL_BIN/$SESSION_NAME"
|
||||
|
||||
# cmd_full 결정 — T4: isolation 디스패치(env prefix / CLI args)를 시작 명령에 주입.
|
||||
# 격리 미사용 시 기존 문자열과 byte-identical (V4 회귀 0).
|
||||
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")"
|
||||
if [ -z "${HERDR_SERVER_NAME:-}" ] || [ "$HERDR_SERVER_NAME" = "default" ]; then
|
||||
export HERDR_SERVER_NAME="multi-agent-mux"
|
||||
fi
|
||||
|
||||
# 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)"
|
||||
fi
|
||||
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")"
|
||||
fi
|
||||
fi
|
||||
@@ -172,25 +151,27 @@ if [ "$(uname)" = "Darwin" ] && [ -f "$RESOLVED_BIN" ]; then
|
||||
fi
|
||||
|
||||
case "$AGENT" in
|
||||
claude) CMD_FULL="${ISO_ENV_PREFIX}${RESOLVED_BIN} --dangerously-skip-permissions" ;;
|
||||
agy) CMD_FULL="${ISO_ENV_PREFIX}${RESOLVED_BIN} --dangerously-skip-permissions" ;;
|
||||
hermes) CMD_FULL="${ISO_ENV_PREFIX}${RESOLVED_BIN}" ;;
|
||||
cline) CMD_FULL="${RESOLVED_BIN} -i${ISO_CMD_ARGS:+ $ISO_CMD_ARGS}" ;;
|
||||
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;;
|
||||
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;;
|
||||
hermes) CMD_FULL="${RESOLVED_BIN}" ;;
|
||||
cline) CMD_FULL="${RESOLVED_BIN} -i" ;;
|
||||
esac
|
||||
|
||||
spawn() {
|
||||
if [ -z "${HERDR_SERVER_NAME:-}" ] || [ "$HERDR_SERVER_NAME" = "default" ]; then
|
||||
export HERDR_SERVER_NAME="multi-agent-mux"
|
||||
fi
|
||||
case "$AGENT" in
|
||||
claude)
|
||||
# 격리 시 wrapper 경로는 env 주입을 운반하지 못하므로 인라인 spawn 강제
|
||||
if [ "$ISOLATE" != "1" ] && { { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$USE_WRAPPER" = "1" ]; }; then
|
||||
if { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$USE_WRAPPER" = "1" ]; then
|
||||
nohup "$WRAPPER" >/dev/null 2>&1 &
|
||||
disown
|
||||
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
|
||||
;;
|
||||
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 ;;
|
||||
esac
|
||||
@@ -209,21 +190,20 @@ cleanup_herdr_on_error() {
|
||||
if [ $exit_code -ne 0 ]; then
|
||||
echo "⚠️ Error occurred during initialization. Rolling back and killing herdr session '$SESSION_NAME'..." >&2
|
||||
_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
|
||||
}
|
||||
trap cleanup_herdr_on_error EXIT
|
||||
|
||||
export HERDR_SERVER_NAME="${HERDR_SERVER_NAME:-$(resolve_herdr_server "$SESSION_NAME")}"
|
||||
|
||||
# TUI 준비 대기
|
||||
if ! wait_for_tui_ready "$SESSION_NAME" "$AGENT"; then
|
||||
echo "ERROR: agent TUI never became ready — aborting (rollback via trap)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$AGENT" = "claude" ]; then
|
||||
handle_startup_dialogs "$SESSION_NAME" 15
|
||||
fi
|
||||
|
||||
# pane 메타 캡처
|
||||
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)
|
||||
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
|
||||
# isolation (HERDR_SERVER_NAME picked up by the lib.sh shim), not a
|
||||
# `--workspace <label>` flag (real `herdr agent start --workspace` wants an
|
||||
# actual workspace id like "w2", which HERDR_SERVER_NAME is not).
|
||||
# herdr server shim (HERDR_SERVER_NAME picked up by the lib.sh shim).
|
||||
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
|
||||
@@ -285,9 +263,7 @@ atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
|
||||
HERDR_EPOCH="$HERDR_EPOCH" PANE_PID="$PANE_PID" PANE_CWD="$PANE_CWD" \
|
||||
CMD_FULL="$CMD_FULL" START_CMD="$START_CMD" CHILD_PID="$CHILD_PID" \
|
||||
HERDR_SERVER_NAME="${HERDR_SERVER_NAME:-default}" \
|
||||
DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" \
|
||||
ISOLATION_UUID="$ISOLATION_UUID" ISOLATION_ROOT="$ISOLATION_ROOT" \
|
||||
ISOLATION_LEVER="$ISOLATION_LEVER" ISOLATION_SEEDED="$ISOLATION_SEEDED" <<'PYEOF'
|
||||
DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" <<'PYEOF'
|
||||
name = os.environ['SESSION_NAME']
|
||||
agent = os.environ['AGENT']
|
||||
role = os.environ['ROLE']
|
||||
@@ -331,15 +307,7 @@ entry = {
|
||||
'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':
|
||||
entry['tui'] = {
|
||||
|
||||
@@ -319,6 +319,7 @@ exec(os.environ['MAM_VERIFY_PY'])
|
||||
|
||||
yaml_path = os.environ['YAML_PATH']
|
||||
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")
|
||||
|
||||
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:
|
||||
continue
|
||||
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}"
|
||||
proj_dir = 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)
|
||||
@@ -563,16 +563,26 @@ for s in d.get('herdr_sessions', []):
|
||||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
if not cwd:
|
||||
continue
|
||||
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"
|
||||
conv_dir = 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)
|
||||
|
||||
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 = []
|
||||
for db in dbs:
|
||||
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)
|
||||
|
||||
if len(valid_candidates) == 1:
|
||||
@@ -597,8 +607,7 @@ for s in d.get('herdr_sessions', []):
|
||||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
if not cwd:
|
||||
continue
|
||||
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"
|
||||
hdb = f"{home}/.hermes/state.db"
|
||||
if not os.path.exists(hdb):
|
||||
continue
|
||||
|
||||
@@ -636,8 +645,7 @@ for s in d.get('herdr_sessions', []):
|
||||
cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
if not cwd:
|
||||
continue
|
||||
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"
|
||||
sessions_dir = f"{home}/.cline/data/sessions"
|
||||
if not os.path.isdir(sessions_dir):
|
||||
continue
|
||||
candidates = []
|
||||
|
||||
@@ -64,7 +64,7 @@ WORKSPACE=/path/to/project
|
||||
AGENT=claude # or agy or hermes
|
||||
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
|
||||
|
||||
# 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"
|
||||
fi
|
||||
|
||||
# 3. Resolve isolation settings for this session (T4/T5 re-apply)
|
||||
# _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
|
||||
# 3. Determine CMD_FULL
|
||||
case "$AGENT" in
|
||||
claude) CMD_FULL="claude --dangerously-skip-permissions -r $UUID" ;;
|
||||
agy) CMD_FULL="agy --dangerously-skip-permissions --conversation $UUID" ;;
|
||||
@@ -130,22 +92,10 @@ case "$AGENT" in
|
||||
cline) CMD_FULL="cline -i --id $UUID" ;;
|
||||
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
|
||||
|
||||
# 4. Spawn new herdr session + run agent with the saved id (and re-applied isolation)
|
||||
# 4. Spawn new herdr session + run agent with the saved id
|
||||
case "$AGENT" in
|
||||
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\" \"$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"
|
||||
# auto-handle trust / bypass dialogs
|
||||
handle_startup_dialogs "$SESSION_NAME" 20
|
||||
|
||||
@@ -9,7 +9,7 @@ usage() {
|
||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes|cline> --session <name> [--dry-run]
|
||||
|
||||
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.
|
||||
EOF
|
||||
}
|
||||
@@ -60,45 +60,6 @@ if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
|
||||
exit 0
|
||||
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)
|
||||
RESOLVED_BIN="$AGENT"
|
||||
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
|
||||
fi
|
||||
|
||||
# Determine CMD_FULL with isolation applied
|
||||
# Determine CMD_FULL
|
||||
case "$AGENT" in
|
||||
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions -r $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 ;;
|
||||
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
|
||||
if [ -f "$RESOLVED_BIN" ] || [[ "$RESOLVED_BIN" == /* ]] || [[ "$RESOLVED_BIN" == ~/* ]]; then
|
||||
if [ ! -x "$RESOLVED_BIN" ]; then
|
||||
|
||||
+3
-5
@@ -53,10 +53,9 @@ $ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \
|
||||
--agent claude \
|
||||
--role developer \
|
||||
--session my-project-dev-claude \
|
||||
--isolate \
|
||||
--herdr-workspace multi-agent-mux
|
||||
--herdr-server multi-agent-mux
|
||||
```
|
||||
* `--isolate` 옵션을 주면 `.mam/agent_homes/<uuid>/` 하위에 로그인 및 설정은 유지하되 대화 내역은 격리되는 홈이 형성됩니다.
|
||||
* 모든 세션은 사용자의 전역 에이전트 설정(Global Config)을 공유하며, herdr 프로세스 격리 및 대화 UUID 단위로 독립 구동됩니다.
|
||||
|
||||
### 2) 세션 접속 (Attach)
|
||||
백그라운드에서 구동된 에이전트 TUI 화면에 들어갑니다.
|
||||
@@ -66,7 +65,7 @@ $ herdr session attach my-project-dev-claude
|
||||
* **화면 탈출**: 대화 중 세션을 유지한 채 터미널로 돌아오려면 `Ctrl + B`를 누른 뒤 `D` 키를 차례로 입력합니다.
|
||||
|
||||
### 3) 에이전트 상태 복원 (Resume)
|
||||
세션이 중지되었거나, 호스트 재기동으로 herdr 서버가 소멸한 경우에도 이전 대화 ID 및 격리 디렉토리를 원자적으로 이어받아 다시 기동할 수 있습니다.
|
||||
세션이 중지되었거나, 호스트 재기동으로 herdr 서버가 소멸한 경우에도 이전 대화 ID를 원자적으로 이어받아 다시 기동할 수 있습니다.
|
||||
```bash
|
||||
# 1단계: 복원 대상 세션의 UUID 자동 조회 (DB/YAML 레지스트리 기반)
|
||||
$ 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; }
|
||||
|
||||
# 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" -- \
|
||||
"claude --dangerously-skip-permissions -r $UUID"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user