Files
shared/scripts/role/role_sleep.sh
T

301 lines
14 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
# ==============================================================================
# 用途:角色的睡眠與記憶整理。由 cron 於睡眠時段每小時觸發(--run),
# 先檢查是否有 AI 正在運行,沒有才進入睡眠並整理記憶:
# NREM 鞏固(分類/去噪/去重/合併/優先度)→ REM 整合(跨記憶
# 連結/抽象化/提取線索)→ 壓縮歸檔 → 日常與其他依使用頻率與優先度遺忘。
# 另提供 --catchupcron 未執行時的補跑)、--force(手動立即整理)、
# --install-cron--remove-cron(排程安裝與移除)、--status(狀態)。
# 更新時間:2026/07/28 12:59:41
# 相依:bash、node、任一 headless CLI、crontab(僅排程安裝需要)、
# 同目錄的 role_lib.sh 與 memory.js。
# 退出碼:0 成功或無事可做;1 參數錯誤或整理失敗(cron 觸發時不影響使用者)。
# ==============================================================================
ROLE_STAGE="role-sleep"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./role_lib.sh
. "${SCRIPT_DIR}/role_lib.sh"
CRON_MARKER="# jsc-role-sleep"
SLEEP_TIMEOUT="${ROLE_SLEEP_TIMEOUT:-180}"
SLEEP_OUTPUT_LIMIT="${ROLE_SLEEP_OUTPUT_LIMIT:-8000}"
usage() {
# 印出用法
cat <<'EOF_USAGE'
用法:role_sleep.sh <模式>
--run cron 觸發:在睡眠時段內且無 AI 運行時整理記憶
--catchup 補跑:cron 未執行時,由 SessionStart hook 於背景呼叫
--force 立即整理一次(忽略時段與 AI 運行檢查)
--install-cron 安裝/更新睡眠排程(每小時檢查一次)
--remove-cron 移除睡眠排程
--status 顯示角色、睡眠時段、排程與記憶統計
EOF_USAGE
}
require_role() {
# 解析角色並確認定義檔存在,取不到時中止
ROLE="$(role_resolve_name)"
[ -n "$ROLE" ] || { role_log "WRN" "未指定角色(ROLE_NAME 與 .active 皆無)"; exit 1; }
[ -f "$(role_file "$ROLE")" ] || { role_log "WRN" "找不到角色定義檔:$(role_file "$ROLE")"; exit 1; }
}
# ------------------------------------------------------------------------------
# 整理主流程
# ------------------------------------------------------------------------------
sleep_cycle() {
# 執行一次完整記憶整理:收集素材 → NREM 鞏固 → REM 整合 → 落檔歸檔 → 遺忘
local reason="$1" cli material prompt result applied forgotten
command -v node >/dev/null 2>&1 || { role_log "ERR" "找不到 node,無法整理記憶"; return 1; }
if ! role_lock_acquire "$ROLE"; then
role_log "WRN" "另一個整理程序正在執行,本次略過(角色 ${ROLE}"
return 0
fi
trap 'role_lock_release "$ROLE"' EXIT
material="$(node "${SCRIPT_DIR}/memory.js" collect --role "$ROLE" 2>/dev/null)"
if [ -z "$material" ]; then
role_log "INF" "沒有待整理記憶(角色 ${ROLE},觸發:${reason}"
node "${SCRIPT_DIR}/memory.js" mark-sleep --role "$ROLE" >/dev/null 2>&1
forgotten="$(node "${SCRIPT_DIR}/memory.js" forget --role "$ROLE" 2>/dev/null)"
role_log "INF" "遺忘檢查:${forgotten}"
role_lock_release "$ROLE"
trap - EXIT
return 0
fi
cli="$(role_select_cli)" || { role_lock_release "$ROLE"; trap - EXIT; return 1; }
prompt="$(cat <<EOF_PROMPT
你是角色「${ROLE}」的睡眠記憶整理器。輸入包含兩段:INBOX(本次待整理的記憶)與 EXISTING(既有記憶索引)。
請模擬睡眠中的兩階段記憶整理,但最後只輸出一個 JSON 物件。
1. 只輸出一個 JSON 物件,不要前言、不要結語、不要 code fence,格式為:
{"memories":[{"action":"new","category":"skill","summary":"一句話總結","tags":["標籤1","標籤2"],"priority":4,"relevance":["explicit","future"],"links":["既有記憶 id"],"memory_type":"procedural","declarative":"implicit","retention_stage":"long_term","sleep_stage":"nrem-rem","content":"- 要點\n- 要點","from":["inbox 的 id"]}],"sleepDigest":"本次睡眠整理摘要,80 字內"}
2. NREM 鞏固階段先做:去除雜訊與流水帳、遮蔽憑證與個資、分類、去重、合併、壓縮成可長期保存的穩定記憶。
3. REM 整合階段再做:找出新記憶與 EXISTING 的關聯,抽出可重複套用的規則、偏好、決策模式、角色語氣調整或未來提取線索。
4. action 三選一:
- new:新的一則記憶。多則 INBOX 講同一件事時合成一筆,from 列出全部來源 id。
- merge:內容已被 EXISTING 中某則涵蓋或重複,填 target 為該既有 id,content 寫合併後的完整內容。
- drop:純雜訊、無保存價值,只需填 from。
5. category 六選一:important(重要)/interest(興趣)/news(新知)/skill(技能)/daily(日常)/other(其他)。
important 放長期偏好、規範、決策與身分背景;interest 放反覆關注的主題;news 放新事實與外部資訊;
skill 放可重複套用的做法;daily 放一次性例行工作;其餘歸 other。
6. priority 必填,1 到 5:5=使用者明確要求、長期規範、穩定偏好或核心身分;4=可重複套用的技能/決策;3=有用新知;2=短期日常;1=低價值但暫存。
7. memory_type 必填,六選一:
- rule:長期規範、固定工作原則。
- preference:穩定偏好、語氣與互動喜好。
- procedural:技能、流程、可重複操作。
- semantic:事實、觀念、工具知識、外部資訊。
- episodic:個別事件、一次性進度、特定時間地點脈絡。
- emotional:情緒反應、語氣連結、制約式喜惡。
8. declarative 必填:semanticepisodicpreferencerule 通常為 explicitproceduralemotional 通常為 implicit。
9. retention_stage 必填:整理後可長期保存者填 long_term;仍只是短期暫存且不值得長期保存者請用 action=drop,不要輸出 working。
10. relevance 必填 1 至 4 個,從下列語意挑選或用等價繁中詞:explicit(使用者明確要求)、future(未來會用)、repeated(反覆出現)、novelty(新知)、emotional(語氣/情緒/偏好)、temporary(短期)。
11. links 可填 EXISTING 中相關記憶 id;沒有就填空陣列。merge 時若有舊 links,應保留並加上新關聯。
12. sleep_stage 填 "nrem"、"rem" 或 "nrem-rem"。只有純分類去噪用 nrem;有建立跨記憶連結或抽象規則用 rem 或 nrem-rem。
13. 感覺記憶(短暫光影、聲音餘響、無結論的工具雜訊)一律 drop;不要保存到長期記憶。
14. **每一則 INBOX 的 id 都必須出現在某一筆的 from 中**,沒被提及的會留到下個睡眠週期重做。
15. content 壓縮成 5 行以內要點(每行以「- 」開頭),總長不超過 400 字,去除重複敘述與流水帳。
16. summary 一句話 40 字內;tags 2 至 4 個。全部使用繁體中文(台灣用語)。
17. sleepDigest 總結本次新增、合併、丟棄、抽象化或建立關聯的重點,80 字內。
18. 嚴禁輸出任何憑證與個資:token、密碼、API key、連線字串、Email、電話、姓名、身分證號。
素材:
${material}
EOF_PROMPT
)"
result="$(role_run_cli "$cli" "$prompt" "$SLEEP_TIMEOUT")"
if [ -z "$result" ]; then
role_log "ERR" "整理結果為空(CLI ${cli}),保留待整理記憶到下個週期"
role_lock_release "$ROLE"
trap - EXIT
return 1
fi
result="$(printf '%s' "$result" | head -c "$SLEEP_OUTPUT_LIMIT" | node "${SCRIPT_DIR}/transcript.js" redact 2>/dev/null)"
applied="$(printf '%s' "$result" | node "${SCRIPT_DIR}/memory.js" apply --role "$ROLE" 2>/dev/null)"
if [ -z "$applied" ]; then
role_log "ERR" "整理結果無法套用(角色 ${ROLE}),保留待整理記憶到下個週期"
role_lock_release "$ROLE"
trap - EXIT
return 1
fi
role_log "INF" "記憶整理完成(角色 ${ROLE},觸發:${reason}):${applied}"
forgotten="$(node "${SCRIPT_DIR}/memory.js" forget --role "$ROLE" 2>/dev/null | tr '\n' '')"
role_log "INF" "遺忘檢查:${forgotten}"
role_lock_release "$ROLE"
trap - EXIT
return 0
}
# ------------------------------------------------------------------------------
# 排程安裝:cron 環境沒有互動 shell 的環境變數,需把必要變數與精簡 PATH 一併寫入
# ------------------------------------------------------------------------------
cron_quote() {
# 把值包成單引號:PATH 等變數常含空白(例如 /mnt/c/Program Files),
# 未加引號會被 cron 的 sh 拆成指令;% 是 cron 的換行符號,一律跳脫。
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g; s/%/\\\\%/g")"
}
cron_path_append() {
# 把單一路徑加入 PATH 清單並去重;cron 單行過長時會拒收 crontab。
local list="$1" item="$2"
[ -n "$item" ] || { printf '%s' "$list"; return 0; }
case ":${list}:" in
*":${item}:"*) printf '%s' "$list" ;;
*) printf '%s%s%s' "$list" "${list:+:}" "$item" ;;
esac
}
cron_path() {
# cron 只需要系統工具、node 與摘要 CLI;避免把互動 shell 的超長 PATH 原樣寫入。
local value="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
local command_path cli
command_path="$(command -v node 2>/dev/null || true)"
[ -n "$command_path" ] && value="$(cron_path_append "$value" "$(dirname "$command_path")")"
cli="$(role_select_cli 2>/dev/null || true)"
if [ -n "$cli" ]; then
command_path="$(command -v "$cli" 2>/dev/null || true)"
[ -n "$command_path" ] && value="$(cron_path_append "$value" "$(dirname "$command_path")")"
fi
printf '%s' "$value"
}
cron_line() {
# 組出 crontab 條目:睡眠時段內每小時檢查一次
local env_prefix="PATH=$(cron_quote "$(cron_path)")"
local var
for var in ROLE_ENABLED ROLE_NAME ROLE_HOME ROLE_MEMORY_HOME ROLE_CLI ROLE_MODEL ROLE_SLEEP_START ROLE_SLEEP_END ROLE_SCOPE; do
if [ -n "${!var:-}" ]; then
env_prefix="${env_prefix} ${var}=$(cron_quote "${!var}")"
fi
done
printf '0 %s * * * %s %s --run >> %s 2>&1 %s\n' \
"$(cron_hours)" "$env_prefix" "$(cron_quote "${SCRIPT_DIR}/role_sleep.sh")" \
"$(cron_quote "$(sleep_log_path)")" "$CRON_MARKER"
}
cron_hours() {
# 依睡眠時段換算 cron 小時欄位(每小時檢查一次,讓 AI 運行中的情況能在下個小時重試)
local start end hour hours=""
start="$(role_time_to_minutes "$(role_sleep_start)")" || { printf '22-23,0-5'; return 0; }
end="$(role_time_to_minutes "$(role_sleep_end)")" || { printf '22-23,0-5'; return 0; }
start=$((start / 60))
end=$((end / 60))
hour="$start"
while [ "$hour" != "$end" ]; do
hours="${hours}${hours:+,}${hour}"
hour=$(((hour + 1) % 24))
done
printf '%s' "${hours:-22,23,0,1,2,3,4,5}"
}
sleep_log_path() {
# 排程輸出的 log 路徑(只記狀態訊息,不含記憶內容)
printf '%s/sleep.log' "$(role_home)"
}
install_cron() {
# 安裝或更新睡眠排程;以 marker 註解辨識自己的條目,不動使用者其他排程
command -v crontab >/dev/null 2>&1 || { role_log "ERR" "找不到 crontab,無法安裝排程"; return 1; }
mkdir -p "$(role_home)" 2>/dev/null
local current new
current="$(crontab -l 2>/dev/null | grep -v -F "$CRON_MARKER")"
new="$(printf '%s\n%s' "$current" "$(cron_line)" | sed '/^$/d')"
printf '%s\n' "$new" | crontab - || { role_log "ERR" "寫入 crontab 失敗"; return 1; }
role_log "INF" "已安裝睡眠排程:每日 $(cron_hours) 時整點檢查(角色 ${ROLE},時段 $(role_sleep_start)$(role_sleep_end)"
role_log "INF" "排程輸出:$(sleep_log_path)"
if ! pgrep -x cron >/dev/null 2>&1 && ! pgrep -x crond >/dev/null 2>&1; then
role_log "WRN" "系統 cron 服務未執行(WSL 常見),排程不會觸發;SessionStart 的背景補跑仍會運作"
fi
return 0
}
remove_cron() {
# 移除本 skill 安裝的排程條目
command -v crontab >/dev/null 2>&1 || { role_log "ERR" "找不到 crontab"; return 1; }
crontab -l 2>/dev/null | grep -v -F "$CRON_MARKER" | crontab -
role_log "INF" "已移除睡眠排程"
return 0
}
show_status() {
# 以表格輸出目前角色與記憶狀態(供 skill 的 --status 使用)
local cron_state="未安裝" cron_service="未執行" window="否"
crontab -l 2>/dev/null | grep -qF "$CRON_MARKER" && cron_state="已安裝"
{ pgrep -x cron >/dev/null 2>&1 || pgrep -x crond >/dev/null 2>&1; } && cron_service="執行中"
role_in_sleep_window && window="是"
printf '| 項目 | 值 |\n| --- | --- |\n'
printf '| 角色 | %s |\n' "$ROLE"
printf '| 角色定義檔 | %s |\n' "$(role_file "$ROLE")"
printf '| 睡眠時段 | %s%s |\n' "$(role_sleep_start)" "$(role_sleep_end)"
printf '| 目前是否睡眠中 | %s |\n' "$window"
printf '| cron 排程 | %s |\n' "$cron_state"
printf '| cron 服務 | %s |\n' "$cron_service"
printf '| 摘要 CLI | %s |\n' "$(role_select_cli 2>/dev/null || printf '找不到可用 CLI')"
printf '\n'
node "${SCRIPT_DIR}/memory.js" stats --role "$ROLE" 2>/dev/null
printf '\n'
}
# ------------------------------------------------------------------------------
# 進入點
# ------------------------------------------------------------------------------
MODE="${1:---status}"
case "$MODE" in
--run)
role_enabled || exit 0
require_role
if ! role_in_sleep_window; then
role_log "DBG" "目前不在睡眠時段($(role_sleep_start)$(role_sleep_end)),略過"
exit 0
fi
if role_ai_running; then
role_log "INF" "偵測到 AI 正在運行,本小時不進入睡眠,下個整點再檢查"
exit 0
fi
sleep_cycle "cron"
;;
--catchup)
role_enabled || exit 0
require_role
if [ "$(node "${SCRIPT_DIR}/memory.js" need-sleep --role "$ROLE" 2>/dev/null)" != "yes" ]; then
role_log "DBG" "不需補跑整理"
exit 0
fi
sleep_cycle "補跑"
;;
--force)
require_role
sleep_cycle "手動"
;;
--install-cron)
require_role
install_cron
;;
--remove-cron)
remove_cron
;;
--status)
require_role
show_status
;;
-h|--help)
usage
;;
*)
usage
exit 1
;;
esac