Files
shared/scripts/role/role_unload.sh
T
JefferyandClaude Opus 5 46caabe4d8 feat(role): 新增點名載入,對話中叫名字即可切換角色
- `UserPromptSubmit` hook(`role_call.sh`)比對訊息開頭的角色名稱/ID/`aliases` 別名,
  命中即注入該角色人格與記憶並接手本輪;未點名時不輸出內容也不寫檔案。
- 抽出 `role_context.sh` 共用 context 組裝,SessionStart 與點名載入共用同一份人格與操作規則,
  避免兩種載入方式漂移;實測 SessionStart 注入內容與改動前 byte-identical。
- 新增階段角色狀態(`~/.roles/.sessions/<工作階段>.role`),`Stop` hook 改以「本階段實際角色」
  寫記憶,避免點名換人後把跟 A 的對話記進 B 的記憶。
- `SessionEnd` 改為釋放本階段名下所有角色鎖並清掉階段狀態檔。
- 切換時先確認取得目標角色鎖才釋放原角色鎖,避免出現兩個角色都沒有的空窗。
- 睡眠時段、目標角色已被其他活躍階段佔用、點名的是已在場的角色時,一律不切換。
- 新增 `ROLE_CALL_ENABLED`(總開關)與 `ROLE_CALL_MARKER_ONLY`(只認 `@名字`)。
- 順帶修正 `role_capture.sh` 的 `--postcompact` 分支在 `PROJECT` 賦值前就使用它,
  導致壓縮摘要記憶的 `sources` 一直為空。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 13:38:25 +08:00

82 lines
4.1 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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_pathsession_idreason
# ------------------------------------------------------------------------------
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