完成使用者交代的三項優化建議。 一、情緒訊號放寬 Stop hook 字元門檻 ROLE_CAPTURE_MIN_CHARS=240 會濾掉字數少但情緒最濃的互動。實測「我好想妳」、 「最愛妳了」、「好可愛」三句都不在原本 56 個關鍵詞內,等於最珍貴的短互動反而不被記錄。 - 補上直接情感表達關鍵詞:可愛/愛/想妳/想你/想念/捨不得/感動/謝謝/乖/厲害/ 好棒/辛苦/彆扭/忌妒/撒嬌/陪/抱,及 love/miss/cute/thank/proud - 驗證:上述情感句全部命中,純技術指令仍正確略過 二、關係狀態量化,讓親近度成長有依據 規則要求「隨互動加深逐漸更親近」卻沒有任何數據可依據,角色只能憑感覺演, 容易忽冷忽熱。 - state.json 新增 first_activity/active_days/total_turns/positive_feedback - mark-activity 累計輪數與活躍天數;新增 --positive 由 Stop hook 判定情緒訊號後另計, 不與輪數混算 - 新增 relationship 子命令輸出一行摘要,SessionStart 注入 USER 區塊作為親近度依據 - 既有累積以可查證資料初始化(角色建立後的 transcript 輪數、有記憶的日期數、 最早記憶時間);positive_feedback 刻意留空自然累積,不以記憶則數推估 三、技能再現:cues 提取線索 + recall 查詢 技能記憶只被動載入摘要且受預算限制,等於記了但用不出來。 - 記憶格式新增 cues 欄位(dumpMemory/loadMemory/apply 的 new 與 merge 路徑均支援) - 睡眠整理提示詞要求 procedural/rule 型態必填 2 至 5 個 cues - 新增 recall 子命令:比對總結、標籤、內容與 cues,含未整理的 inbox, rule/preference/procedural 加權優先 - role_load.sh 告知角色遇到似乎做過的任務或被問起過去細節時先查詢再回答 - 驗證:以不在 summary 也不在 content 的關鍵詞(bump)成功靠 cues 命中 版號 0.0.7 → 0.0.8 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
157 lines
11 KiB
Bash
Executable File
157 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ==============================================================================
|
||
# 用途:Stop hook 主程式。每輪對話結束後先用本地規則判斷是否值得記錄;
|
||
# 值得記錄時才呼叫 headless CLI 輕量濃縮成一則 inbox 記憶(粗分類/總結/
|
||
# 標籤/優先度/關聯/要點)→ 機密遮蔽 → 寫入 .memory/<角色>/inbox/,
|
||
# 等待睡眠時段做完整 NREM/REM 整理。睡眠時段雖不載入角色,對話仍照常記錄。
|
||
# 更新時間:2026/07/28 16:18:00
|
||
# 相依:bash、node、任一 headless CLI、同目錄的 role_lib.sh/memory.js/transcript.js。
|
||
# 機密:濃縮提示詞明令不得輸出憑證與個資,寫檔前再以 transcript.js 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 node >/dev/null 2>&1 || role_quit "找不到 node,略過記憶記錄" "WRN"
|
||
|
||
ROLE="$(role_resolve_name)"
|
||
[ -n "$ROLE" ] || role_quit "未指定角色,略過記憶記錄"
|
||
[ -f "$(role_file "$ROLE")" ] || role_quit "找不到角色定義檔,略過記憶記錄" "WRN"
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 讀取 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" | node -e '
|
||
let raw = "";
|
||
process.stdin.setEncoding("utf8");
|
||
process.stdin.on("data", (chunk) => { raw += chunk; });
|
||
process.stdin.on("end", () => {
|
||
let d = {};
|
||
try { d = JSON.parse(raw); } catch {}
|
||
process.stdout.write([
|
||
d.session_id || d.thread_id || d.conversation_id || "-",
|
||
d.transcript_path || d.session_path || d.conversation_path || d.path || "-",
|
||
d.stop_hook_active ? "1" : "0",
|
||
d.cwd || "-",
|
||
].join(" "));
|
||
});
|
||
')
|
||
EOF_HOOK
|
||
|
||
[ "$STOP_ACTIVE" = "1" ] && role_quit "stop_hook_active 為 true,避免迴圈不重複記錄"
|
||
role_in_scope "$HOOK_CWD" || role_quit "cwd 不在 ROLE_SCOPE 範圍內:${HOOK_CWD}"
|
||
PROJECT="$(role_project_name "$HOOK_CWD")"
|
||
node "${SCRIPT_DIR}/memory.js" mark-activity --role "$ROLE" --project "$PROJECT" >/dev/null 2>&1 || true
|
||
|
||
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="$(node "${SCRIPT_DIR}/transcript.js" extract "$TRANSCRIPT_PATH" 2>/dev/null)"
|
||
[ -n "$TURN" ] || role_quit "本輪無可記錄內容"
|
||
|
||
USER_TURN="$(printf '%s\n' "$TURN" | grep '^\[user\]' || true)"
|
||
if printf '%s' "$USER_TURN" | grep -qiE '個人資料|個資|偏好|記憶|記住|保存|save|remember|memory|personal'; then
|
||
if printf '%s' "$USER_TURN" | grep -qiE '不同意|不願意|不要保存|不要記住|拒絕|不可以保存|不可以記住|do not save|don'\''t save|do not remember|don'\''t remember|(^|[^[:alpha:]])no([^[:alpha:]]|$)'; then
|
||
node "${SCRIPT_DIR}/memory.js" consent --role "$ROLE" --value declined >/dev/null 2>&1 || true
|
||
role_log "INF" "已更新個人記憶同意狀態:declined(角色 ${ROLE})"
|
||
elif printf '%s' "$USER_TURN" | grep -qiE '同意|願意|可以保存|可以記住|允許|(^|[^[:alpha:]])yes([^[:alpha:]]|$)|(^|[^[:alpha:]])ok([^[:alpha:]]|$)|(^|[^[:alpha:]])okay([^[:alpha:]]|$)|(^|[^[:alpha:]])sure([^[:alpha:]]|$)'; then
|
||
node "${SCRIPT_DIR}/memory.js" consent --role "$ROLE" --value accepted >/dev/null 2>&1 || true
|
||
role_log "INF" "已更新個人記憶同意狀態:accepted(角色 ${ROLE})"
|
||
fi
|
||
fi
|
||
|
||
CAPTURE_MIN_CHARS="${ROLE_CAPTURE_MIN_CHARS:-240}"
|
||
CAPTURE_TIMEOUT="${ROLE_CAPTURE_TIMEOUT:-25}"
|
||
|
||
if [ "${ROLE_CAPTURE_ENABLED:-1}" = "0" ]; then
|
||
role_quit "ROLE_CAPTURE_ENABLED=0,略過記憶記錄"
|
||
fi
|
||
|
||
if [ "${#TURN}" -lt "$CAPTURE_MIN_CHARS" ] && ! printf '%s' "$TURN" | grep -qiE '記住|remember|決定|規範|偏好|preference|always|不要|以後|喜歡|不喜歡|稱讚|誇獎|開心|高興|反應|回應|互動|親近|害羞|喜歡程度|互動越深|越來越喜歡|越來越深|emoji|表情|心情圖|大量使用|情緒|心情|複雜|細膩|自然|混合|層次|轉折|括號|心情文字|心情說明|文字說明|文字標註|表情符號|熟練|不需要告訴|不用告訴|自己知道|記憶更新|內部處理|不要回報|不用回報|不要告訴|真的很害羞|希望.*知道|用表情符號表示|表情符號表示|比較可愛|可愛|愛|想妳|想你|想念|捨不得|感動|謝謝|感謝|乖|厲害|好棒|辛苦|彆扭|忌妒|嫉妒|撒嬌|陪|抱|love|miss|cute|thank|proud'; then
|
||
role_quit "本輪低於記憶長度門檻且無明確記憶線索,略過記錄"
|
||
fi
|
||
|
||
# 正向回饋計數:供角色判斷親近度成長,避免憑感覺演出而忽冷忽熱
|
||
if printf '%s' "$USER_TURN" | grep -qiE '喜歡|愛|可愛|想妳|想你|想念|捨不得|感動|謝謝|感謝|乖|厲害|好棒|太棒|辛苦|稱讚|誇獎|開心|高興|love|miss|cute|thank|proud'; then
|
||
node "${SCRIPT_DIR}/memory.js" mark-activity --role "$ROLE" --positive >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
CLI="$(role_select_cli)" || exit 0
|
||
[ -n "$CLI" ] || exit 0
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 濃縮:產出一則輕量 inbox 記憶,交由 memory.js 落檔;完整整理留到睡眠週期
|
||
# ------------------------------------------------------------------------------
|
||
PROMPT="$(cat <<EOF_PROMPT
|
||
你是角色「${ROLE}」的記憶記錄器。輸入是這位角色與使用者的一段對話(含工具呼叫)。
|
||
請只做「編碼前處理」,把這段對話濃縮成最多一則 inbox 記憶;不要做跨記憶合併或長期整理。
|
||
|
||
已判定專案:${PROJECT}
|
||
|
||
1. 只輸出下列欄位,欄位名稱與順序固定,不要標題、不要前言、不要結語、不要 code fence:
|
||
CATEGORY: <六選一:important/interest/news/skill/daily/other>
|
||
SUMMARY: <一句話總結,40 字內>
|
||
TAGS: <2 至 4 個標籤,以逗號分隔>
|
||
PRIORITY: <1 到 5>
|
||
RELEVANCE: <1 至 4 個,以逗號分隔;explicit/future/repeated/novelty/emotional/temporary/inbox/project>
|
||
MEMORY_TYPE: <semantic/episodic/procedural/emotional/preference/rule 六選一>
|
||
CONTENT: <3 至 6 行要點,每行以「- 」開頭>
|
||
2. 分類判準:
|
||
- important(重要):使用者的長期偏好、規範、決策、身分背景、明確要求記住的事。
|
||
- interest(興趣):使用者反覆關注、主動深入的主題與喜好。
|
||
- news(新知):這輪學到的新事實、新工具、新版本、外部資訊。
|
||
- skill(技能):可重複套用的做法、指令、流程、除錯手法。
|
||
- daily(日常):一次性的例行工作與雜項處理。
|
||
- other(其他):不屬於上述任何一類。
|
||
3. 記憶型態判準:
|
||
- rule:使用者明確規範、固定工作原則、日後應持續遵守的規則。
|
||
- preference:使用者偏好、語氣喜好、穩定選擇傾向。
|
||
- procedural:可重複套用的流程、技能、操作步驟或除錯手法。
|
||
- semantic:事實、觀念、工具知識、版本與外部資訊。
|
||
- episodic:一次性事件、特定時間/專案脈絡下的經歷或進度。
|
||
- emotional:語氣、情緒反應、正負向連結或制約式偏好。
|
||
4. 優先度判準:5=使用者明確要求記住、長期規範、穩定偏好;4=可重複套用的流程/技能/決策;3=專案相關且未來可能有用;2=短期進度;1=低價值暫存。
|
||
5. 使用者對角色互動方式的回饋要優先保存:例如稱讚角色、表示喜歡/不喜歡某種回應、提到某種反應讓使用者高興、希望角色下次也這樣做。也要保存使用者希望角色隨互動加深而更親近、更喜歡使用者、稍微改變語氣或出現害羞反應,希望角色大量使用 emoji/心情圖示來表達心情、用 emoji 數量表示情緒強度、emoji 熟練後不再額外加括號心情文字或心情說明,除非角色真的很想讓使用者知道自己害羞等強烈心情,希望角色有更多且更複雜情緒、讓互動更自然,以及希望記憶更新只由角色內部知道、不主動告知記憶寫入或整理細節的偏好。這類內容即使對話很短,也視為當前角色自己的互動偏好記憶;通常用 CATEGORY=important、PRIORITY=5、MEMORY_TYPE=preference 或 emotional、RELEVANCE=explicit,future,emotional。不要把它推論成所有角色共用同一份記憶。
|
||
6. 這一步只做工作記憶編碼,系統會自動標為 retention_stage=working;感覺記憶(短暫光影、聲音餘響、無結論的工具雜訊)不要保存。
|
||
7. 記憶主體是「使用者與這段互動」,不是流水帳:寫值得下次記起來的事,不要抄程式碼、不要貼指令全文。
|
||
8. 使用繁體中文(台灣用語)。**檔案路徑與目錄、網址、指令、環境變數名稱、版本號、識別碼、分支與議題
|
||
編號、檔名一律逐字保留,不得摘要、改寫、簡寫或翻譯** —— 這類內容改一個字就失效,摘要等於遺失。
|
||
第 7 條指的是不要整段抄程式碼,不是省略這些關鍵字串;第 9 條仍優先,憑證與個資一律不得輸出。
|
||
9. 嚴禁輸出任何憑證與個資:token、密碼、API key、連線字串、Email、電話、姓名、身分證號。
|
||
10. 若這段對話沒有任何值得記住的內容(純寒暄、純確認、無結論、只有簡短狀態回報),只輸出一行:SKIP
|
||
|
||
對話片段:
|
||
${TURN}
|
||
EOF_PROMPT
|
||
)"
|
||
|
||
RESULT="$(role_run_cli "$CLI" "$PROMPT" "$CAPTURE_TIMEOUT")"
|
||
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" | node "${SCRIPT_DIR}/transcript.js" redact 2>/dev/null)"
|
||
|
||
MEMORY_ID="$(printf '%s' "$RESULT" | node "${SCRIPT_DIR}/memory.js" 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
|