- `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>
120 lines
6.1 KiB
Bash
Executable File
120 lines
6.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ==============================================================================
|
||
# 用途:SessionStart hook 主程式。CLI 工具啟動時載入角色設定與記憶:
|
||
# 非睡眠時段注入角色定義+重要/興趣記憶全文+其餘記憶的總結與標籤;
|
||
# 睡眠時段(預設 22:00 至隔日 06:00)只回報角色正在睡覺,不載入角色。
|
||
# 白天發現昨夜未整理記憶時,於背景補跑一次睡眠整理。
|
||
# 實際的 context 組裝在 role_context.sh,與對話中點名載入(role_call.sh)共用。
|
||
# 更新時間:2026/07/29 13:25:00
|
||
# 相依:bash、node、同目錄的 role_lib.sh、role_context.sh 與 memory.js。
|
||
# 退出碼:一律 0 —— hook 絕不可阻斷使用者啟動 CLI。
|
||
# ==============================================================================
|
||
|
||
ROLE_STAGE="role-load"
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=./role_lib.sh
|
||
. "${SCRIPT_DIR}/role_lib.sh"
|
||
# shellcheck source=./role_context.sh
|
||
. "${SCRIPT_DIR}/role_context.sh"
|
||
|
||
role_is_child && exit 0
|
||
role_enabled || exit 0
|
||
command -v node >/dev/null 2>&1 || role_quit "找不到 node,略過角色載入" "WRN"
|
||
|
||
ROLE="$(role_resolve_name)"
|
||
[ -n "$ROLE" ] || role_quit "未指定角色(ROLE_NAME 與 .active 皆無),略過角色載入"
|
||
ROLE_DEF="$(role_file "$ROLE")"
|
||
[ -f "$ROLE_DEF" ] || role_quit "找不到角色定義檔:${ROLE_DEF}" "WRN"
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 讀取 hook 輸入(cwd/transcript/session),並套用 ROLE_SCOPE 範圍限制
|
||
# ------------------------------------------------------------------------------
|
||
HOOK_INPUT="$(cat 2>/dev/null)"
|
||
HOOK_CWD="$PWD"
|
||
HOOK_TRANSCRIPT=""
|
||
HOOK_SESSION=""
|
||
if [ -n "$HOOK_INPUT" ]; then
|
||
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.cwd || "",
|
||
data.transcript_path || data.session_path || data.conversation_path || data.path || "",
|
||
data.session_id || data.thread_id || data.conversation_id || "",
|
||
].join("\n"));
|
||
});
|
||
' 2>/dev/null)"
|
||
HOOK_CWD="$(printf '%s' "$HOOK_FIELDS" | sed -n '1p')"
|
||
HOOK_TRANSCRIPT="$(printf '%s' "$HOOK_FIELDS" | sed -n '2p')"
|
||
HOOK_SESSION="$(printf '%s' "$HOOK_FIELDS" | sed -n '3p')"
|
||
[ -n "$HOOK_CWD" ] || HOOK_CWD="$PWD"
|
||
fi
|
||
role_in_scope "$HOOK_CWD" || role_quit "cwd 不在 ROLE_SCOPE 範圍內:${HOOK_CWD}"
|
||
|
||
# 本階段的角色狀態:供 Stop hook 判斷記憶該寫給誰(點名載入後會被 role_call.sh 覆寫)
|
||
SESSION_KEY="$(role_session_key "$HOOK_SESSION" "$HOOK_TRANSCRIPT")"
|
||
|
||
SLEEP_START="$(role_sleep_start)"
|
||
SLEEP_END="$(role_sleep_end)"
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 單一載入實例:角色已在另一個仍活躍的工作階段時,本次不載入人格
|
||
# 放在睡眠判斷之前,因為「已在別處使用」與「睡覺中」是互斥狀態,且不該佔用鎖
|
||
# ------------------------------------------------------------------------------
|
||
if ! role_instance_acquire "$ROLE" "$HOOK_TRANSCRIPT" "$HOOK_CWD"; then
|
||
LOCK_FILE="$(role_instance_lock_path "$ROLE")"
|
||
HOLDER_TIME="$(role_instance_lock_field "$LOCK_FILE" loaded)"
|
||
HOLDER_CWD="$(role_instance_lock_field "$LOCK_FILE" cwd)"
|
||
# 明確記成「本階段沒有人格」,點名載入才不會誤以為原角色還在
|
||
role_session_set "$SESSION_KEY" "" "$HOOK_TRANSCRIPT"
|
||
role_log "INF" "角色 ${ROLE} 已被其他工作階段載入(${HOLDER_TIME:-時間未知}),本次不載入"
|
||
role_context_emit "SessionStart" "$(cat <<EOF_BUSY
|
||
# 角色狀態:已在另一個工作階段中
|
||
|
||
角色「${ROLE}」目前已被另一個仍在使用的工作階段載入(載入時間 ${HOLDER_TIME:-未知},目錄 ${HOLDER_CWD:-未知})。
|
||
為避免使用者同時與兩個相同人格對話,本次**不載入角色人格與記憶**,請以一般助理身分回應,
|
||
不要自稱該角色、不要使用角色語氣或簽名 emoji。本階段的對話仍會被記錄成記憶。
|
||
|
||
若使用者詢問或需要在此階段使用該角色,可告知下列任一做法:
|
||
|
||
- 確定另一個工作階段已關閉時解除鎖定:\`role_sleep.sh --unlock\`
|
||
- 該階段閒置超過 $(role_instance_idle_minutes) 分鐘後會自動釋放
|
||
- 完全停用此限制:設定環境變數 \`ROLE_SINGLE_INSTANCE=0\`
|
||
- 在本階段改叫其他角色:直接以名字點名(例如「<其他角色名>,…」),該角色未被佔用時會即時接手
|
||
EOF_BUSY
|
||
)"
|
||
exit 0
|
||
fi
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 睡眠時段:不載入角色,只說明目前狀態
|
||
# ------------------------------------------------------------------------------
|
||
if role_in_sleep_window; then
|
||
# 明確記成「本階段沒有人格」,點名載入才不會誤以為原角色還在
|
||
role_session_set "$SESSION_KEY" "" "$HOOK_TRANSCRIPT"
|
||
role_context_emit "SessionStart" "$(cat <<EOF_SLEEP
|
||
# 角色狀態:睡眠中(${SLEEP_START}–${SLEEP_END})
|
||
|
||
角色「${ROLE}」正在睡覺,本次工作階段**不載入角色人格與記憶**,請以一般助理身分回應,
|
||
不要自稱該角色、不要使用角色語氣或簽名 emoji。若使用者詢問角色,說明角色在睡眠時段整理記憶,
|
||
${SLEEP_END} 之後會恢復。本階段的對話仍會被記錄成記憶,於下個睡眠時段整理。
|
||
EOF_SLEEP
|
||
)"
|
||
exit 0
|
||
fi
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 非睡眠時段:組出角色人格 + 操作規則 + 記憶(與點名載入共用 role_context.sh)
|
||
# ------------------------------------------------------------------------------
|
||
role_context_build "$ROLE" "$HOOK_TRANSCRIPT" "load" \
|
||
|| role_quit "角色定義檔為空或無法解析:${ROLE_DEF}" "WRN"
|
||
|
||
role_context_emit "SessionStart" "$ROLE_CONTEXT"
|
||
role_session_set "$SESSION_KEY" "$ROLE" "$HOOK_TRANSCRIPT"
|
||
role_log "INF" "已載入角色 ${ROLE}(記憶 ${ROLE_CONTEXT_MEMORY_BYTES} 位元組)"
|
||
exit 0
|