feat(role): SessionEnd 立即釋放角色鎖,不必等閒置逾時
關掉 CLI 後立刻重開新階段時,角色會被自己上一個階段的殘留鎖擋住: 原本只有「持有者 transcript 閒置超過 30 分鐘」與手動 --unlock 兩條釋放 路徑,而剛關閉的 transcript mtime 還很新,機制無法區分「已關閉」與 「正在別的視窗打字」,因此最久要等 30 分鐘才叫得回角色。 新增 SessionEnd hook(role_unload.sh)作為快速路徑,工作階段正常結束時 立即刪鎖。mtime 閒置逾時後援保留不動 —— SessionEnd 不保證觸發 (kill -9、直接關終端機、WSL 關機、當機都不會跑),少了後援會在異常 結束時把角色鎖死到下次手動解鎖,兩條路徑缺一不可。 只在鎖檔登記的 transcript 等於自己時才釋放:被鎖擋下的第二個階段結束時 同樣會觸發 SessionEnd,若無條件刪鎖會把仍在使用中的第一個階段的鎖一起 刪掉,等於讓單一實例限制形同虛設。無法取得 transcript、sub agent (ROLE_SKIP_INSTANCE_LOCK=1)、限制已停用(ROLE_SINGLE_INSTANCE=0) 一律不動鎖。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
# 用途:角色(role)系統的共用函式庫。提供統一 log、啟用判斷、角色解析、
|
||||
# 睡眠時段判斷、AI 行程偵測、摘要 CLI 選擇與呼叫、記憶目錄鎖。
|
||||
# 本檔僅供 source,不可直接執行。
|
||||
# 更新時間:2026/07/28 12:21:11
|
||||
# 更新時間:2026/07/29 12:55:00
|
||||
# 相依:bash;摘要路徑需 README 定義的任一 headless CLI。
|
||||
# 機密:不 echo 任何 token;角色與記憶內容僅在程序記憶體與檔案間傳遞。
|
||||
# ==============================================================================
|
||||
@@ -274,9 +274,18 @@ role_lock_release() {
|
||||
#
|
||||
# 目的:同一角色同時只被一個工作階段載入,避免使用者同時與兩個相同人格對話。
|
||||
#
|
||||
# 為什麼以 transcript 檔的 mtime 判斷而非 pid:SessionStart hook 無法可靠取得 CLI 主行程
|
||||
# 的 pid,且沒有保證會觸發的 SessionEnd hook 可用來釋放鎖。活躍的工作階段會持續寫入
|
||||
# transcript,因此「該檔多久沒被寫入」是最貼近真實狀態、也不需要清理程序的判斷依據。
|
||||
# 釋放分兩條路,兩者缺一不可:
|
||||
# 1. 快速路徑:SessionEnd hook(role_unload.sh)在工作階段結束時刪掉自己的鎖,讓使用者
|
||||
# 關掉 CLI 後可以立刻重開新階段叫回角色。
|
||||
# 2. 後援:以下的 mtime 閒置逾時接手。SessionEnd **不保證觸發**(kill -9、直接關終端機
|
||||
# 視窗、WSL 關機、當機都不會跑),少了它會在異常結束時把角色鎖死到下次手動解鎖。
|
||||
#
|
||||
# 為什麼後援以 transcript 檔的 mtime 判斷而非 pid:SessionStart hook 無法可靠取得 CLI 主
|
||||
# 行程的 pid。活躍的工作階段會持續寫入 transcript,因此「該檔多久沒被寫入」是最貼近真實
|
||||
# 狀態、也不需要清理程序的判斷依據。
|
||||
#
|
||||
# 已知取捨:正常關閉才有快速路徑;異常結束仍需等 ROLE_INSTANCE_IDLE_MINUTES(預設 30 分鐘)
|
||||
# 過期,或手動 role_sleep.sh --unlock。
|
||||
#
|
||||
# 設計原則:**寧可誤放行也不要誤鎖** —— 誤鎖的後果是使用者叫不出角色,比偶爾重複載入嚴重。
|
||||
# 因此無法識別工作階段(例如 hook 未提供 transcript 路徑)時一律放行。
|
||||
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# 用途:SessionEnd hook 主程式。工作階段結束時**盡力**釋放角色單一載入鎖,讓使用者
|
||||
# 關掉 CLI 後可以立刻在新階段叫回同一個角色,不必等閒置逾時自然過期。
|
||||
# 更新時間:2026/07/29 12:55:00
|
||||
# 相依:bash、node(解析 hook 輸入)、同目錄的 role_lib.sh。
|
||||
# 退出碼:一律 0 —— hook 絕不可阻斷 CLI 結束。
|
||||
#
|
||||
# 為什麼這只是「快速路徑」而非唯一解法:SessionEnd 不保證觸發(kill -9、直接關掉終端機
|
||||
# 視窗、WSL 關機、當機都不會跑),因此 role_instance_acquire 的 mtime 閒置逾時接手仍是
|
||||
# 最終保障,兩者缺一不可 —— 只留 SessionEnd 會在異常結束時把角色鎖死到下次手動解鎖。
|
||||
#
|
||||
# 為什麼一定要比對 transcript 才釋放:被鎖擋下的第二個工作階段也會觸發 SessionEnd,
|
||||
# 若無條件刪鎖,它關閉時就會把「仍在使用中」的第一個階段的鎖一起刪掉,等於讓整個
|
||||
# 單一實例限制形同虛設。只有鎖確實登記在自己名下時才釋放。
|
||||
# ==============================================================================
|
||||
|
||||
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 || 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"
|
||||
|
||||
ROLE="$(role_resolve_name)"
|
||||
[ -n "$ROLE" ] || role_quit "未指定角色,略過角色鎖釋放"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 讀取 hook 輸入(transcript_path/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 || "",
|
||||
].join("\n"));
|
||||
});
|
||||
' 2>/dev/null)"
|
||||
HOOK_TRANSCRIPT="$(printf '%s' "$HOOK_FIELDS" | sed -n '1p')"
|
||||
HOOK_REASON="$(printf '%s' "$HOOK_FIELDS" | sed -n '2p')"
|
||||
|
||||
# 無法識別工作階段就不動鎖:寧可讓它照原本的閒置逾時過期,也不要誤刪別人的鎖
|
||||
[ -n "$HOOK_TRANSCRIPT" ] || role_quit "hook 未提供 transcript 路徑,略過角色鎖釋放"
|
||||
|
||||
LOCK_FILE="$(role_instance_lock_path "$ROLE")"
|
||||
[ -f "$LOCK_FILE" ] || role_quit "角色 ${ROLE} 目前無鎖,無須釋放"
|
||||
|
||||
HOLDER="$(role_instance_lock_field "$LOCK_FILE" transcript)"
|
||||
if [ "$HOLDER" != "$HOOK_TRANSCRIPT" ]; then
|
||||
role_quit "角色鎖屬於其他工作階段,不釋放(持有者 ${HOLDER:-未知})"
|
||||
fi
|
||||
|
||||
role_instance_release "$ROLE"
|
||||
role_log "INF" "工作階段結束(原因 ${HOOK_REASON:-未提供}),已釋放角色鎖:${ROLE}"
|
||||
exit 0
|
||||
Reference in New Issue
Block a user