SessionStart 載入角色與記憶、Stop 記錄對話成記憶,睡眠時段(預設 22:00–06:00) 由 cron 排程整理:分類六類、去重合併、設標籤與一句話總結、壓縮歸檔, 日常與其他依使用頻率遺忘;cron 未執行時由啟動路徑背景補跑。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
112 lines
5.4 KiB
Bash
Executable File
112 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ==============================================================================
|
||
# 用途:Stop hook 主程式。每輪對話結束後抽出本輪內容 → 呼叫 headless CLI 濃縮成
|
||
# 一則記憶(分類/總結/標籤/要點)→ 機密遮蔽 → 寫入 .memory/<角色>/inbox/,
|
||
# 等待睡眠時段整理。睡眠時段雖不載入角色,對話仍照常記錄。
|
||
# 更新時間:2026/07/28 00:00:00
|
||
# 相依:bash、python3、任一 headless CLI、同目錄的 role_lib.sh/memory.py/transcript.py。
|
||
# 機密:濃縮提示詞明令不得輸出憑證與個資,寫檔前再以 transcript.py redact 遮蔽一次。
|
||
# 退出碼:一律 0 —— hook 絕不可阻斷使用者流程。
|
||
# ==============================================================================
|
||
|
||
ROLE_STAGE="role-capture"
|
||
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
|
||
command -v python3 >/dev/null 2>&1 || role_quit "找不到 python3,略過記憶記錄" "WRN"
|
||
|
||
ROLE="$(role_resolve_name)"
|
||
[ -n "$ROLE" ] || role_quit "未指定角色,略過記憶記錄"
|
||
[ -f "$(role_file "$ROLE")" ] || role_quit "找不到角色定義檔,略過記憶記錄" "WRN"
|
||
|
||
CLI="$(role_select_cli)" || exit 0
|
||
[ -n "$CLI" ] || exit 0
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 讀取 hook 傳入的 JSON(session_id/transcript_path/cwd/stop_hook_active)
|
||
# ------------------------------------------------------------------------------
|
||
HOOK_INPUT="$(cat)"
|
||
[ -n "$HOOK_INPUT" ] || role_quit "hook 輸入為空,略過記憶記錄" "WRN"
|
||
|
||
read -r SESSION_ID TRANSCRIPT_PATH STOP_ACTIVE HOOK_CWD <<EOF_HOOK
|
||
$(printf '%s' "$HOOK_INPUT" | python3 -c '
|
||
import json, sys
|
||
try:
|
||
d = json.load(sys.stdin)
|
||
except ValueError:
|
||
d = {}
|
||
print(
|
||
d.get("session_id") or d.get("thread_id") or d.get("conversation_id") or "-",
|
||
d.get("transcript_path") or d.get("session_path") or d.get("conversation_path") or d.get("path") or "-",
|
||
"1" if d.get("stop_hook_active") else "0",
|
||
d.get("cwd", "") or "-",
|
||
)
|
||
')
|
||
EOF_HOOK
|
||
|
||
[ "$STOP_ACTIVE" = "1" ] && role_quit "stop_hook_active 為 true,避免迴圈不重複記錄"
|
||
role_in_scope "$HOOK_CWD" || role_quit "cwd 不在 ROLE_SCOPE 範圍內:${HOOK_CWD}"
|
||
|
||
if [ ! -f "$TRANSCRIPT_PATH" ] && [ -n "${CODEX_THREAD_ID:-}" ]; then
|
||
TRANSCRIPT_PATH="$(find "${HOME}/.codex/sessions" -type f -name "*${CODEX_THREAD_ID}.jsonl" -print -quit 2>/dev/null)"
|
||
[ -n "$TRANSCRIPT_PATH" ] || TRANSCRIPT_PATH="-"
|
||
fi
|
||
[ -f "$TRANSCRIPT_PATH" ] || role_quit "找不到 transcript:${TRANSCRIPT_PATH}" "WRN"
|
||
|
||
TURN="$(python3 "${SCRIPT_DIR}/transcript.py" extract "$TRANSCRIPT_PATH" 2>/dev/null)"
|
||
[ -n "$TURN" ] || role_quit "本輪無可記錄內容"
|
||
|
||
PROJECT="$(role_project_name "$HOOK_CWD")"
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 濃縮:產出一則記憶(四欄固定格式),交由 memory.py 落檔
|
||
# ------------------------------------------------------------------------------
|
||
PROMPT="$(cat <<EOF_PROMPT
|
||
你是角色「${ROLE}」的記憶記錄器。輸入是這位角色與使用者的一段對話(含工具呼叫)。
|
||
請把這段對話濃縮成「一則記憶」,規則:
|
||
|
||
已判定專案:${PROJECT}
|
||
|
||
1. 只輸出下列四個欄位,欄位名稱與順序固定,不要標題、不要前言、不要結語、不要 code fence:
|
||
CATEGORY: <六選一:important/interest/news/skill/daily/other>
|
||
SUMMARY: <一句話總結,40 字內>
|
||
TAGS: <2 至 4 個標籤,以逗號分隔>
|
||
CONTENT: <3 至 6 行要點,每行以「- 」開頭>
|
||
2. 分類判準:
|
||
- important(重要):使用者的長期偏好、規範、決策、身分背景、明確要求記住的事。
|
||
- interest(興趣):使用者反覆關注、主動深入的主題與喜好。
|
||
- news(新知):這輪學到的新事實、新工具、新版本、外部資訊。
|
||
- skill(技能):可重複套用的做法、指令、流程、除錯手法。
|
||
- daily(日常):一次性的例行工作與雜項處理。
|
||
- other(其他):不屬於上述任何一類。
|
||
3. 記憶主體是「使用者與這段互動」,不是流水帳:寫值得下次記起來的事,不要抄程式碼、不要貼指令全文。
|
||
4. 使用繁體中文(台灣用語),保留關鍵事實:檔案/專案/指令/數量/分支/議題編號。
|
||
5. 嚴禁輸出任何憑證與個資:token、密碼、API key、連線字串、Email、電話、姓名、身分證號。
|
||
6. 若這段對話沒有任何值得記住的內容(純寒暄、純確認、無結論),只輸出一行:SKIP
|
||
|
||
對話片段:
|
||
${TURN}
|
||
EOF_PROMPT
|
||
)"
|
||
|
||
RESULT="$(role_run_cli "$CLI" "$PROMPT" 45)"
|
||
if [ -z "$RESULT" ]; then
|
||
role_log "WRN" "記憶濃縮產出為空(CLI ${CLI}),略過本輪"
|
||
exit 0
|
||
fi
|
||
printf '%s' "$RESULT" | grep -qiE '^\s*SKIP\s*$' && role_quit "判定本輪無值得記住的內容"
|
||
|
||
# 第二道防線:對模型輸出再遮蔽一次機密與個資
|
||
RESULT="$(printf '%s' "$RESULT" | python3 "${SCRIPT_DIR}/transcript.py" redact 2>/dev/null)"
|
||
|
||
MEMORY_ID="$(printf '%s' "$RESULT" | python3 "${SCRIPT_DIR}/memory.py" write --role "$ROLE" --project "$PROJECT" 2>/dev/null)"
|
||
if [ -n "$MEMORY_ID" ]; then
|
||
role_log "INF" "已記錄記憶 ${MEMORY_ID}(角色 ${ROLE},專案 ${PROJECT},CLI ${CLI})"
|
||
else
|
||
role_log "WRN" "記憶寫入失敗或內容不足(角色 ${ROLE})"
|
||
fi
|
||
exit 0
|