feat(stop): fully purge registry entries on stop --purge-conversation and add zombie pre-gate

This commit is contained in:
2026-07-16 12:06:37 +09:00
parent dfdbf69187
commit 230e262414
@@ -27,8 +27,10 @@
# Exit codes: # Exit codes:
# 0 = success (or already-stopped no-op) | 1 = YAML not found / not registered # 0 = success (or already-stopped no-op) | 1 = YAML not found / not registered
# 2 = invalid args | 3 = interactive confirmation required (--yes 누락) # 2 = invalid args | 3 = interactive confirmation required (--yes 누락)
# 4 = purge aborted (tmux session survived the kill chain)
set -euo pipefail set -euo pipefail
# shellcheck disable=SC1091
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh" source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
usage() { usage() {
@@ -68,7 +70,8 @@ done
[ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; usage; exit 2; } [ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; usage; exit 2; }
[ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; } [ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; }
export TMUX_SERVER_NAME="$(resolve_tmux_server "$SESSION_NAME")" TMUX_SERVER_NAME="$(resolve_tmux_server "$SESSION_NAME")"
export TMUX_SERVER_NAME
# --agent 미지정 시 이름 suffix 로 fallback (P1-F) # --agent 미지정 시 이름 suffix 로 fallback (P1-F)
if [ -z "$AGENT" ]; then if [ -z "$AGENT" ]; then
@@ -190,6 +193,21 @@ else
echo "tmux already dead, just updating YAML" echo "tmux already dead, just updating YAML"
fi fi
# Purge pre-gate: 레코드 제거는 tmux 사망이 확인된 경우에만 허용한다.
# (kill 체인은 best-effort — 세션이 살아남으면 monitor drift-B 가
# 레코드 없는 세션을 running 으로 자동 재등록해 purge 가 조용히 뒤집힌다)
if [ "$PURGE" = "1" ] && [ "$TMUX_ALIVE" = "1" ]; then
_wait_session_gone "$SESSION_NAME" 5 || true # SIGKILL 폴백의 비동기 회수 윈도우 흡수
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
echo "ERROR: session '$SESSION_NAME' is still alive after the kill chain." >&2
echo " Refusing registry removal — records preserved (no state was modified)." >&2
echo " Diagnose the stuck TUI (tmux attach -t '$SESSION_NAME'), then re-run" >&2
echo " stop_session.sh --purge-conversation --yes (retry is safe/idempotent)." >&2
delegate_publish_event "$DELEGATE_JOB_ID" error "purge aborted: tmux session still alive"
exit 4
fi
fi
atomic_dump_yaml "$AGENT_SESSIONS_YAML" \ atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
SESSION_NAME="$SESSION_NAME" AGENT="$AGENT" PURGE="$PURGE" \ SESSION_NAME="$SESSION_NAME" AGENT="$AGENT" PURGE="$PURGE" \
NOW_ISO="$NOW_ISO" NOW_EPOCH="$NOW_EPOCH" LAST_STATUS="$LAST_STATUS" \ NOW_ISO="$NOW_ISO" NOW_EPOCH="$NOW_EPOCH" LAST_STATUS="$LAST_STATUS" \
@@ -216,12 +234,7 @@ if target is None:
print(f"ERROR: disappeared during script: {name}", flush=True) print(f"ERROR: disappeared during script: {name}", flush=True)
raise SystemExit(1) raise SystemExit(1)
if purge: if not purge:
target['status'] = 'terminated'
target['terminated_at'] = now
target['terminated_at_epoch'] = int(os.environ['NOW_EPOCH'])
target['termination_mode'] = 'purge'
else:
target['status'] = 'stopped' target['status'] = 'stopped'
target['stopped_at'] = now target['stopped_at'] = now
target['stopped_at_epoch'] = int(os.environ['NOW_EPOCH']) target['stopped_at_epoch'] = int(os.environ['NOW_EPOCH'])
@@ -324,9 +337,13 @@ elif purge and not purge_uuid:
print("WARN: --purge-conversation requested but no workspace-scoped UUID resolved; nothing purged", flush=True) print("WARN: --purge-conversation requested but no workspace-scoped UUID resolved; nothing purged", flush=True)
if purge: if purge:
target['resumable'] = False d['tmux_sessions'] = [s for s in d.get('tmux_sessions', []) if s.get('name') != name]
if purge_uuid:
print(f"updated: {name} status={target['status']}", flush=True) print(f"removed: {name} (registry entry fully purged from YAML+DB)", flush=True)
else:
print(f"removed: {name} (registry entry purged from YAML+DB; WARN: conversation artifacts unresolved — disk copies may remain)", flush=True)
else:
print(f"updated: {name} status={target['status']}", flush=True)
PYEOF PYEOF
delegate_publish_event "$DELEGATE_JOB_ID" completed "session terminated" delegate_publish_event "$DELEGATE_JOB_ID" completed "session terminated"