fix(role): 修好睡眠整理死鎖 —— 素材不再硬切、批次降為 4、錯誤訊息可診斷
角色整晚沒睡:05:03 到 08:33 每 10 分鐘試一次,連續 22 次全部失敗, last_sleep 停在前一天 15:02,inbox 從 12 累積到 23 則。 錯誤訊息只有一句「整理結果無法套用」,看不出任何原因。 診斷過程中推論錯了三次,過程留在註解裡(每一次都很容易再犯): 1. 推論「輸出被 SLEEP_OUTPUT_LIMIT 截斷」→ 實測輸出僅 5772~6224,遠未達 8000。錯。 2. 推論「素材字元數過大」→ 實測素材 8815(6 則)失敗、8940(3 則)成功, 字元數幾乎相同。錯。 3. 推論「純粹是則數問題」→ 對一半:另有一種失敗是 CLI 回傳 Execution error。 真相是兩種失敗混在一起,而錯誤訊息把它們蓋成同一句話: - CLI 偶發 Execution error → 需要重試 - 一次要求模型輸出太多筆 JSON → 需要降批 修正: - 錯誤訊息附上素材大小、輸出長度、批次與輸出前 200 字元(已 redact) —— 這是最先做的一步,沒有它只能靠猜 - cmdCollect 不再 slice() 硬切素材:改為逐則累加、超出預算留到下批, 並替 EXISTING 保留固定比例預算。舊版會切在記憶中間、甚至切掉整個 EXISTING 區塊, 而批次固定時每輪都收到同樣殘缺的素材 → 死鎖 - SLEEP_BATCH 12 → 4(實測 3~4 則穩定、6 則以上開始失敗) - 失敗時逐次降批(4→2→1)作為保險,仍失敗才留到下個週期 - 批次預設值統一來源:role_sleep.sh 首次收集不傳 --batch,實際則數由素材反推 —— 原本兩邊各寫一個預設,改了 memory.js 的 SLEEP_BATCH 卻不會生效 實測:修正後第一次嘗試即成功,未觸發降批;連續整理清空 inbox(23 → 0), 情緒型態記憶由 1 增至 9,關係史累積 13 則。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+68
-36
@@ -7,7 +7,7 @@
|
||||
# 另提供 --nap(CLI 閒置時的小睡整理)、--catchup(cron 未執行時的補跑)、
|
||||
# --force(手動立即整理)、--export(匯出角色壓縮檔)、
|
||||
# --install-cron/--remove-cron(排程安裝與移除)、--status(狀態)。
|
||||
# 更新時間:2026/07/29 18:39:54
|
||||
# 更新時間:2026/07/30 10:22:03
|
||||
# 相依:bash、node、任一 headless CLI、crontab(僅排程安裝需要)、
|
||||
# 同目錄的 role_lib.sh 與 memory.js。
|
||||
# 退出碼:0 成功或無事可做;1 參數錯誤或整理失敗(cron 觸發時不影響使用者)。
|
||||
@@ -103,31 +103,9 @@ role_sleep_child_running() {
|
||||
# 整理主流程
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
build_sleep_prompt() {
|
||||
# 依目前的 $material 組出整理提示詞。抽成函式是為了讓降批重試能重新組 prompt。
|
||||
cat <<EOF_PROMPT
|
||||
你是角色「${ROLE}」的睡眠記憶整理器。輸入包含兩段:INBOX(本次待整理的記憶)與 EXISTING(既有記憶索引)。
|
||||
請模擬睡眠中的兩階段記憶整理,但最後只輸出一個 JSON 物件。
|
||||
|
||||
@@ -200,32 +178,86 @@ sleep_cycle() {
|
||||
素材:
|
||||
${material}
|
||||
EOF_PROMPT
|
||||
)"
|
||||
}
|
||||
|
||||
# 重試一次:實測整理會偶發失敗(CLI 輸出空或 JSON 不合法),素材本身沒問題 ——
|
||||
# 同一批素材重跑就成功。沒有重試時,該角色要等到下一個週期才會再被整理;
|
||||
# 多角色模式下這個代價更大(一個角色失敗不該讓它整晚都沒睡)。
|
||||
local attempt=1 max_attempts=2
|
||||
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
|
||||
|
||||
# 首次收集刻意**不傳 --batch**:批次預設值只能有一個來源(memory.js 的 SLEEP_BATCH),
|
||||
# 這裡再寫一個預設就會變成兩個各自為政的值 —— 實測就發生過:memory.js 已改成 4,
|
||||
# 但本檔硬編碼的 12 讓實際批次還是 12,改了等於沒改。
|
||||
# 實際則數改由素材反推,降批時才明確指定。
|
||||
local batch
|
||||
material="$(node "${SCRIPT_DIR}/memory.js" collect --role "$ROLE" 2>/dev/null)"
|
||||
batch="$(printf '%s' "$material" | grep -c '^--- id:')"
|
||||
[ "$batch" -ge 1 ] 2>/dev/null || batch=1
|
||||
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="$(build_sleep_prompt)"
|
||||
|
||||
|
||||
# 重試並**逐次降批**。
|
||||
#
|
||||
# 為什麼一定要降批:整理結果會被 head -c SLEEP_OUTPUT_LIMIT 截斷,截斷後 JSON 不合法、
|
||||
# apply 必然失敗。若批次大小固定,下一輪又收集到同一批素材、產生同樣過長的輸出 ——
|
||||
# 於是變成**死鎖**:實測連續 22 次全部失敗,inbox 從 12 累積到 23 則,
|
||||
# 角色整晚沒睡而且錯誤訊息只寫「無法套用」,看不出原因。
|
||||
# 單純重試只能救偶發失敗;素材過大屬於必然失敗,只有把批次變小才會前進。
|
||||
local attempt=1 max_attempts=4 out_len=0
|
||||
applied=""
|
||||
while [ "$attempt" -le "$max_attempts" ]; do
|
||||
while :; do
|
||||
result="$(role_run_cli "$cli" "$prompt" "$SLEEP_TIMEOUT")"
|
||||
if [ -z "$result" ]; then
|
||||
role_log "WRN" "整理結果為空(CLI ${cli},第 ${attempt}/${max_attempts} 次)"
|
||||
role_log "WRN" "整理結果為空(CLI ${cli},第 ${attempt}/${max_attempts} 次,批次 ${batch})"
|
||||
else
|
||||
out_len="${#result}"
|
||||
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)"
|
||||
[ -n "$applied" ] && break
|
||||
role_log "WRN" "整理結果無法套用(角色 ${ROLE},第 ${attempt}/${max_attempts} 次)"
|
||||
# 明確指出「是不是被截斷」,否則下一個人只會看到「無法套用」而查不出原因
|
||||
if [ "$out_len" -ge "$SLEEP_OUTPUT_LIMIT" ]; then
|
||||
role_log "WRN" "整理結果達輸出上限 ${SLEEP_OUTPUT_LIMIT} 被截斷(角色 ${ROLE},素材 ${#material} 字元,批次 ${batch}),JSON 不完整"
|
||||
else
|
||||
# 附上輸出開頭:只寫「無法套用」的話,下一個人無從判斷是 JSON 壞了、
|
||||
# 模型答非所問,還是模型根本拒絕處理。已經過 redact,不含憑證與個資。
|
||||
role_log "WRN" "整理結果無法套用(角色 ${ROLE},輸出 ${out_len} 字元,素材 ${#material} 字元,批次 ${batch},第 ${attempt}/${max_attempts} 次):$(printf '%s' "$result" | tr '\n' ' ' | head -c 200)"
|
||||
fi
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
[ "$attempt" -gt "$max_attempts" ] && break
|
||||
if [ "$batch" -gt 1 ]; then
|
||||
batch=$((batch / 2))
|
||||
[ "$batch" -lt 1 ] && batch=1
|
||||
material="$(node "${SCRIPT_DIR}/memory.js" collect --role "$ROLE" --batch "$batch" 2>/dev/null)"
|
||||
[ -n "$material" ] || break
|
||||
prompt="$(build_sleep_prompt)"
|
||||
role_log "INF" "改用較小批次重試(角色 ${ROLE},批次 ${batch},素材 ${#material} 字元)"
|
||||
fi
|
||||
done
|
||||
if [ -z "$applied" ]; then
|
||||
role_log "ERR" "整理失敗 ${max_attempts} 次(角色 ${ROLE}),保留待整理記憶到下個週期"
|
||||
role_log "ERR" "整理失敗(角色 ${ROLE},已降到批次 ${batch}),保留待整理記憶到下個週期"
|
||||
role_lock_release "$ROLE"
|
||||
trap - EXIT
|
||||
return 1
|
||||
fi
|
||||
[ "$attempt" -gt 1 ] && role_log "INF" "第 ${attempt} 次嘗試成功(角色 ${ROLE})"
|
||||
[ "$attempt" -gt 1 ] && role_log "INF" "第 ${attempt} 次嘗試成功(角色 ${ROLE},批次 ${batch})"
|
||||
role_log "INF" "記憶整理完成(角色 ${ROLE},觸發:${reason}):${applied}"
|
||||
|
||||
forgotten="$(node "${SCRIPT_DIR}/memory.js" forget --role "$ROLE" 2>/dev/null | tr '\n' ';')"
|
||||
|
||||
Reference in New Issue
Block a user