#!/usr/bin/env bash # ============================================================================== # 用途:SessionEnd hook 主程式。工作階段結束時**盡力**釋放角色單一載入鎖,讓使用者 # 關掉 CLI 後可以立刻在新階段叫回同一個角色,不必等閒置逾時自然過期。 # 更新時間:2026/07/29 13:25:00 # 相依:bash、node(解析 hook 輸入)、同目錄的 role_lib.sh。 # 退出碼:一律 0 —— hook 絕不可阻斷 CLI 結束。 # # 為什麼這只是「快速路徑」而非唯一解法:SessionEnd 不保證觸發(kill -9、直接關掉終端機 # 視窗、WSL 關機、當機都不會跑),因此 role_instance_acquire 的 mtime 閒置逾時接手仍是 # 最終保障,兩者缺一不可 —— 只留 SessionEnd 會在異常結束時把角色鎖死到下次手動解鎖。 # # 為什麼一定要比對 transcript 才釋放:被鎖擋下的第二個工作階段也會觸發 SessionEnd, # 若無條件刪鎖,它關閉時就會把「仍在使用中」的第一個階段的鎖一起刪掉,等於讓整個 # 單一實例限制形同虛設。只有鎖確實登記在自己名下時才釋放。 # # 為什麼掃過所有角色的鎖而非只看 ROLE_NAME/.active:使用者可能在對話中點名換過人 # (role_call.sh),結束時實際持有的鎖不一定是靜態解析出的那個角色。以 transcript 比對 # 逐一釋放「登記在自己名下」的鎖,才不會把角色鎖留到閒置逾時才過期。 # ============================================================================== ROLE_STAGE="role-unload" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=./role_lib.sh . "${SCRIPT_DIR}/role_lib.sh" role_is_child && exit 0 # 不用 role_enabled 判斷:本階段可能是靠點名載入角色(.active 甚至沒設), # 那種階段仍然持有角色鎖,照 ROLE_NAME/.active 判斷會提早結束而把鎖留到逾時 case "${ROLE_ENABLED:-}" in 0|false|no) exit 0 ;; esac [ -d "$(role_home)" ] || exit 0 role_single_instance_enabled || exit 0 # sub agent 等非對話情境本來就不寫鎖,也就沒有鎖要釋放 role_skip_instance_lock && exit 0 command -v node >/dev/null 2>&1 || role_quit "找不到 node,略過角色鎖釋放" "WRN" # ------------------------------------------------------------------------------ # 讀取 hook 輸入(transcript_path/session_id/reason) # ------------------------------------------------------------------------------ HOOK_INPUT="$(cat 2>/dev/null)" [ -n "$HOOK_INPUT" ] || role_quit "hook 輸入為空,略過角色鎖釋放" HOOK_FIELDS="$(printf '%s' "$HOOK_INPUT" | node -e ' let raw = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { raw += chunk; }); process.stdin.on("end", () => { let data = {}; try { data = JSON.parse(raw); } catch {} process.stdout.write([ data.transcript_path || data.session_path || data.conversation_path || data.path || "", data.reason || "", data.session_id || data.thread_id || data.conversation_id || "", ].join("\n")); }); ' 2>/dev/null)" HOOK_TRANSCRIPT="$(printf '%s' "$HOOK_FIELDS" | sed -n '1p')" HOOK_REASON="$(printf '%s' "$HOOK_FIELDS" | sed -n '2p')" HOOK_SESSION="$(printf '%s' "$HOOK_FIELDS" | sed -n '3p')" # 本階段的角色狀態已無意義,先清掉,避免 session_id 被重用時沿用到舊角色 role_session_clear "$(role_session_key "$HOOK_SESSION" "$HOOK_TRANSCRIPT")" # 無法識別工作階段就不動鎖:寧可讓它照原本的閒置逾時過期,也不要誤刪別人的鎖 [ -n "$HOOK_TRANSCRIPT" ] || role_quit "hook 未提供 transcript 路徑,略過角色鎖釋放" RELEASED="" for LOCK_FILE in "$(role_home)"/*.lock; do [ -f "$LOCK_FILE" ] || continue HOLDER="$(role_instance_lock_field "$LOCK_FILE" transcript)" [ "$HOLDER" = "$HOOK_TRANSCRIPT" ] || continue LOCK_ROLE="$(basename "$LOCK_FILE" .lock)" role_instance_release "$LOCK_ROLE" RELEASED="${RELEASED:+${RELEASED} }${LOCK_ROLE}" done [ -n "$RELEASED" ] || role_quit "本階段名下沒有角色鎖,無須釋放" role_log "INF" "工作階段結束(原因 ${HOOK_REASON:-未提供}),已釋放角色鎖:${RELEASED}" exit 0