fix(role): 睡眠整理死鎖修復 + 繁簡防線(0.1.5) #29
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-generic",
|
"name": "jsc-generic",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"description": "JSC 跨 AI 助理共用規範 plugin(Claude Code / Codex / Antigravity / OpenCode)。所有 skills 以 SKILL.md 為共通標準,於 Claude Code 以 /jsc-generic: 前綴呼叫。",
|
"description": "JSC 跨 AI 助理共用規範 plugin(Claude Code / Codex / Antigravity / OpenCode)。所有 skills 以 SKILL.md 為共通標準,於 Claude Code 以 /jsc-generic: 前綴呼叫。",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-generic",
|
"name": "jsc-generic",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"description": "JSC 跨 AI 助理共用規範 plugin。所有 skills 以 SKILL.md 為共通標準。",
|
"description": "JSC 跨 AI 助理共用規範 plugin。所有 skills 以 SKILL.md 為共通標準。",
|
||||||
"skills": "./skills"
|
"skills": "./skills"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-generic",
|
"name": "jsc-generic",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"description": "JSC 跨 AI 助理共用規範 plugin。所有 skills 以 SKILL.md 為共通標準;於 Antigravity 以 /jsc-generic: 前綴呼叫。",
|
"description": "JSC 跨 AI 助理共用規範 plugin。所有 skills 以 SKILL.md 為共通標準;於 Antigravity 以 /jsc-generic: 前綴呼叫。",
|
||||||
"skills": "./skills/"
|
"skills": "./skills/"
|
||||||
}
|
}
|
||||||
|
|||||||
+82
-32
@@ -5,7 +5,7 @@
|
|||||||
// 記憶交接,使用獨立字元預算),(3) 睡眠整理時輸出待整理
|
// 記憶交接,使用獨立字元預算),(3) 睡眠整理時輸出待整理
|
||||||
// 素材並套用整理結果(NREM 鞏固/REM 整合、分類、去重、標籤、總結、
|
// 素材並套用整理結果(NREM 鞏固/REM 整合、分類、去重、標籤、總結、
|
||||||
// 優先度、關聯、壓縮歸檔),(4) 依使用頻率與優先度遺忘日常與其他類記憶。
|
// 優先度、關聯、壓縮歸檔),(4) 依使用頻率與優先度遺忘日常與其他類記憶。
|
||||||
// 更新時間:2026/07/29 18:56:33
|
// 更新時間:2026/07/30 10:22:03
|
||||||
// 相依:Node.js 標準庫。
|
// 相依:Node.js 標準庫。
|
||||||
// 退出碼:0 成功;1 無內容可處理;2 參數錯誤。呼叫端(hook)一律不得因此中斷。
|
// 退出碼:0 成功;1 無內容可處理;2 參數錯誤。呼叫端(hook)一律不得因此中斷。
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
@@ -102,12 +102,28 @@ function warnIfSimplified(role, where, ...texts) {
|
|||||||
const DECLARATIVE_VALUES = ["explicit", "implicit"];
|
const DECLARATIVE_VALUES = ["explicit", "implicit"];
|
||||||
const RETENTION_STAGES = ["working", "long_term"];
|
const RETENTION_STAGES = ["working", "long_term"];
|
||||||
|
|
||||||
// 單批上限刻意壓在 12:每則整理結果約需 650 字元(summary/content/各欄位),
|
// 單批則數。這個值被實測修正過三次,過程值得留著:
|
||||||
// 而 SLEEP_OUTPUT_LIMIT 預設 8000,8000÷650≈12。原本預設 60 與輸出上限矛盾 ——
|
//
|
||||||
// 實測 25 則的素材達 25798 位元組、輸出 JSON 被截斷成不合法格式,整批整理直接失敗。
|
// 1. 原本預設 60,與 SLEEP_OUTPUT_LIMIT 8000 矛盾 —— 25 則的素材達 25798 位元組,
|
||||||
// 寧可分多批各自成功,也不要一次做完卻全部失敗。
|
// 輸出 JSON 被截斷成不合法格式,整批整理直接失敗。於是壓到 12。
|
||||||
const SLEEP_BATCH = 12;
|
// 2. 12 仍然失敗,且連續 22 次全部失敗(inbox 從 12 累積到 23 則,角色整晚沒睡)。
|
||||||
const COLLECT_LIMIT = 12000;
|
// 當時推論是「輸出被截斷」,但實測輸出只有 5772~6224 字元、遠未達 8000 上限 —— 推論錯。
|
||||||
|
// 3. 再推論是「素材字元數過大」,把 COLLECT_LIMIT 降到 9000。結果素材 8815(6 則)失敗、
|
||||||
|
// 素材 8940(3 則)成功 —— 字元數幾乎相同,差別只在**則數**。推論再次錯。
|
||||||
|
//
|
||||||
|
// 結論:限制因素是「一次要求模型輸出幾筆結構化 JSON」,不是素材或輸出的字元量。
|
||||||
|
// 則數越多,模型在中途寫壞引號或轉義的機率越高,而 JSON 壞一個字元整批就套用失敗。
|
||||||
|
// 實測 3~4 則穩定成功、6 則以上開始失敗,因此預設 4。
|
||||||
|
//
|
||||||
|
// 批次變小的代價是清空 inbox 需要更多輪,但每輪成功率高得多,
|
||||||
|
// 總成本反而低於「一次做很多、失敗後靠降批重跑三趟」。
|
||||||
|
const SLEEP_BATCH = 4;
|
||||||
|
// 素材字元預算。實測界線約在 10000:素材 9739 與 4592 都成功,11009 與 11834 都失敗
|
||||||
|
// (失敗形式是模型輸出的 JSON 不合法,且輸出長度遠未達 SLEEP_OUTPUT_LIMIT,
|
||||||
|
// 所以不是輸出被截斷,而是一次要求整理太多則時模型的結構化輸出品質下降)。
|
||||||
|
// 預設值原為 12000 —— 剛好落在必然失敗的區間,於是每輪都得靠降批重試才成功,
|
||||||
|
// 白跑兩趟 CLI。降到 9000 讓第一次嘗試就落在可成功的範圍,降批機制只作為保險。
|
||||||
|
const COLLECT_LIMIT = 9000;
|
||||||
const EXISTING_INDEX_LIMIT = 120;
|
const EXISTING_INDEX_LIMIT = 120;
|
||||||
const CONTENT_LIMIT = 1200;
|
const CONTENT_LIMIT = 1200;
|
||||||
const DEFAULT_LOAD_LIMIT = 4000;
|
const DEFAULT_LOAD_LIMIT = 4000;
|
||||||
@@ -713,24 +729,53 @@ function cmdCollect(args) {
|
|||||||
const collectLimit = Math.max(2000, args.limit);
|
const collectLimit = Math.max(2000, args.limit);
|
||||||
const existingLimit = Math.max(0, args.existingLimit);
|
const existingLimit = Math.max(0, args.existingLimit);
|
||||||
const all = listInbox(args.role);
|
const all = listInbox(args.role);
|
||||||
const inbox = all.slice(0, batchSize);
|
if (!all.length) return 1;
|
||||||
if (!inbox.length) return 1;
|
|
||||||
// 明確標示本批未處理的量,避免使用者誤以為已全部整理完
|
|
||||||
const deferred = all.length - inbox.length;
|
|
||||||
|
|
||||||
const lines = deferred > 0
|
// 素材必須是**結構完整**的,寧可少收一則也不能切斷。
|
||||||
? [`=== INBOX(待整理,每則以 id 標識;本批 ${inbox.length} 則,另有 ${deferred} 則留待下批)===`]
|
//
|
||||||
: ["=== INBOX(待整理,每則以 id 標識)==="];
|
// 舊版是先把整份素材組好、最後 text.slice(0, collectLimit) 硬切 —— 實測會切在某則記憶
|
||||||
for (const [meta, content] of inbox) {
|
// 內容中間,甚至把整個 EXISTING 區塊切掉。LLM 拿到殘缺素材就產出不合法的 JSON,
|
||||||
lines.push(`--- id: ${meta.id} | 時間: ${meta.created || "-"} ---`);
|
// apply 必然失敗;而批次大小固定,下一輪又收到同樣被截斷的素材 → **死鎖**:
|
||||||
lines.push(`初判分類: ${CATEGORY_LABELS[meta.category || "other"] || "其他"}`);
|
// 實測連續 22 次全部失敗、inbox 從 12 累積到 23 則,角色整晚沒睡。
|
||||||
lines.push(`初判總結: ${meta.summary || ""}`);
|
//
|
||||||
lines.push(`初判標籤: ${(meta.tags || []).join("、") || "無"}`);
|
// 因此改為逐則累加、超出預算就留到下批,並替 EXISTING 保留固定比例的預算。
|
||||||
lines.push(`初判優先度: ${normalizePriority(meta.priority, meta.category || "other")}`);
|
const inboxBudget = Math.max(1000, Math.floor(collectLimit * 0.75));
|
||||||
lines.push(`初判關聯: ${(meta.relevance || []).join("、") || "-"}`);
|
|
||||||
lines.push(`初判記憶型態: ${meta.memory_type} / ${meta.declarative} / ${meta.retention_stage}`);
|
const blocks = [];
|
||||||
lines.push("內容:", content, "");
|
let used = 0;
|
||||||
|
let taken = 0;
|
||||||
|
for (const [meta, content] of all.slice(0, batchSize)) {
|
||||||
|
let body = content;
|
||||||
|
const head = [
|
||||||
|
`--- id: ${meta.id} | 時間: ${meta.created || "-"} ---`,
|
||||||
|
`初判分類: ${CATEGORY_LABELS[meta.category || "other"] || "其他"}`,
|
||||||
|
`初判總結: ${meta.summary || ""}`,
|
||||||
|
`初判標籤: ${(meta.tags || []).join("、") || "無"}`,
|
||||||
|
`初判優先度: ${normalizePriority(meta.priority, meta.category || "other")}`,
|
||||||
|
`初判關聯: ${(meta.relevance || []).join("、") || "-"}`,
|
||||||
|
`初判記憶型態: ${meta.memory_type} / ${meta.declarative} / ${meta.retention_stage}`,
|
||||||
|
"內容:",
|
||||||
|
].join("\n");
|
||||||
|
// 單則就超出預算時(極長記憶)才截斷它自己的內容,並明講截斷了 ——
|
||||||
|
// 這是保底,否則批次降到 1 仍然過大就永遠無法前進。
|
||||||
|
if (taken === 0 && head.length + body.length > inboxBudget) {
|
||||||
|
body = `${body.slice(0, Math.max(200, inboxBudget - head.length - 40))}\n…(本則內容過長已截斷)…`;
|
||||||
}
|
}
|
||||||
|
const chunk = `${head}\n${body}\n`;
|
||||||
|
// 第一則一定收,否則整批永遠不會前進
|
||||||
|
if (taken > 0 && used + chunk.length > inboxBudget) break;
|
||||||
|
blocks.push(chunk);
|
||||||
|
used += chunk.length + 1;
|
||||||
|
taken += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deferred = all.length - taken;
|
||||||
|
const lines = [
|
||||||
|
deferred > 0
|
||||||
|
? `=== INBOX(待整理,每則以 id 標識;本批 ${taken} 則,另有 ${deferred} 則留待下批)===`
|
||||||
|
: "=== INBOX(待整理,每則以 id 標識)===",
|
||||||
|
...blocks,
|
||||||
|
];
|
||||||
|
|
||||||
lines.push("=== EXISTING(既有記憶索引,供去重與合併判斷)===");
|
lines.push("=== EXISTING(既有記憶索引,供去重與合併判斷)===");
|
||||||
const rows = [];
|
const rows = [];
|
||||||
@@ -740,18 +785,23 @@ function cmdCollect(args) {
|
|||||||
rows.push(`- id: ${meta.id} | 分類: ${CATEGORY_LABELS[category]} | 優先度: ${normalizePriority(meta.priority, category)} | 型態: ${meta.memory_type}/${meta.declarative}/${meta.retention_stage} | 標籤: ${tags} | 關聯: ${(meta.relevance || []).join("、") || "-"} | links: ${(meta.links || []).join("、") || "-"} | 總結: ${meta.summary || ""}`);
|
rows.push(`- id: ${meta.id} | 分類: ${CATEGORY_LABELS[category]} | 優先度: ${normalizePriority(meta.priority, category)} | 型態: ${meta.memory_type}/${meta.declarative}/${meta.retention_stage} | 標籤: ${tags} | 關聯: ${(meta.relevance || []).join("、") || "-"} | links: ${(meta.links || []).join("、") || "-"} | 總結: ${meta.summary || ""}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (rows.length) {
|
if (!rows.length) {
|
||||||
lines.push(...rows.slice(0, existingLimit));
|
|
||||||
if (rows.length > existingLimit) lines.push(`…(既有記憶索引超過 ${existingLimit} 則,已依優先度與更新時間截斷)…`);
|
|
||||||
} else {
|
|
||||||
lines.push("(尚無既有記憶)");
|
lines.push("(尚無既有記憶)");
|
||||||
|
} else {
|
||||||
|
// EXISTING 用剩下的預算,逐行加到放不下為止;整行放不下就不放,不切半行
|
||||||
|
let remain = collectLimit - lines.join("\n").length;
|
||||||
|
let shown = 0;
|
||||||
|
for (const row of rows.slice(0, existingLimit)) {
|
||||||
|
if (row.length + 1 > remain) break;
|
||||||
|
lines.push(row);
|
||||||
|
remain -= row.length + 1;
|
||||||
|
shown += 1;
|
||||||
|
}
|
||||||
|
const omitted = Math.min(rows.length, existingLimit) - shown + Math.max(0, rows.length - existingLimit);
|
||||||
|
if (omitted > 0) lines.push(`…(既有記憶索引另有 ${omitted} 則未列入,已依優先度與更新時間排序)…`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = lines.join("\n");
|
process.stdout.write(lines.join("\n"));
|
||||||
if (text.length > collectLimit) {
|
|
||||||
text = `${text.slice(0, collectLimit)}\n…(睡眠整理素材超過 ${collectLimit} 字元預算已截斷,其餘留待下個睡眠週期)…`;
|
|
||||||
}
|
|
||||||
process.stdout.write(text);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+68
-36
@@ -7,7 +7,7 @@
|
|||||||
# 另提供 --nap(CLI 閒置時的小睡整理)、--catchup(cron 未執行時的補跑)、
|
# 另提供 --nap(CLI 閒置時的小睡整理)、--catchup(cron 未執行時的補跑)、
|
||||||
# --force(手動立即整理)、--export(匯出角色壓縮檔)、
|
# --force(手動立即整理)、--export(匯出角色壓縮檔)、
|
||||||
# --install-cron/--remove-cron(排程安裝與移除)、--status(狀態)。
|
# --install-cron/--remove-cron(排程安裝與移除)、--status(狀態)。
|
||||||
# 更新時間:2026/07/29 18:39:54
|
# 更新時間:2026/07/30 10:22:03
|
||||||
# 相依:bash、node、任一 headless CLI、crontab(僅排程安裝需要)、
|
# 相依:bash、node、任一 headless CLI、crontab(僅排程安裝需要)、
|
||||||
# 同目錄的 role_lib.sh 與 memory.js。
|
# 同目錄的 role_lib.sh 與 memory.js。
|
||||||
# 退出碼:0 成功或無事可做;1 參數錯誤或整理失敗(cron 觸發時不影響使用者)。
|
# 退出碼:0 成功或無事可做;1 參數錯誤或整理失敗(cron 觸發時不影響使用者)。
|
||||||
@@ -103,31 +103,9 @@ role_sleep_child_running() {
|
|||||||
# 整理主流程
|
# 整理主流程
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
sleep_cycle() {
|
build_sleep_prompt() {
|
||||||
# 執行一次完整記憶整理:收集素材 → NREM 鞏固 → REM 整合 → 落檔歸檔 → 遺忘
|
# 依目前的 $material 組出整理提示詞。抽成函式是為了讓降批重試能重新組 prompt。
|
||||||
local reason="$1" cli material prompt result applied forgotten
|
cat <<EOF_PROMPT
|
||||||
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(既有記憶索引)。
|
你是角色「${ROLE}」的睡眠記憶整理器。輸入包含兩段:INBOX(本次待整理的記憶)與 EXISTING(既有記憶索引)。
|
||||||
請模擬睡眠中的兩階段記憶整理,但最後只輸出一個 JSON 物件。
|
請模擬睡眠中的兩階段記憶整理,但最後只輸出一個 JSON 物件。
|
||||||
|
|
||||||
@@ -200,32 +178,86 @@ sleep_cycle() {
|
|||||||
素材:
|
素材:
|
||||||
${material}
|
${material}
|
||||||
EOF_PROMPT
|
EOF_PROMPT
|
||||||
)"
|
}
|
||||||
|
|
||||||
# 重試一次:實測整理會偶發失敗(CLI 輸出空或 JSON 不合法),素材本身沒問題 ——
|
sleep_cycle() {
|
||||||
# 同一批素材重跑就成功。沒有重試時,該角色要等到下一個週期才會再被整理;
|
# 執行一次完整記憶整理:收集素材 → NREM 鞏固 → REM 整合 → 落檔歸檔 → 遺忘
|
||||||
# 多角色模式下這個代價更大(一個角色失敗不該讓它整晚都沒睡)。
|
local reason="$1" cli material prompt result applied forgotten
|
||||||
local attempt=1 max_attempts=2
|
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=""
|
applied=""
|
||||||
while [ "$attempt" -le "$max_attempts" ]; do
|
while :; do
|
||||||
result="$(role_run_cli "$cli" "$prompt" "$SLEEP_TIMEOUT")"
|
result="$(role_run_cli "$cli" "$prompt" "$SLEEP_TIMEOUT")"
|
||||||
if [ -z "$result" ]; then
|
if [ -z "$result" ]; then
|
||||||
role_log "WRN" "整理結果為空(CLI ${cli},第 ${attempt}/${max_attempts} 次)"
|
role_log "WRN" "整理結果為空(CLI ${cli},第 ${attempt}/${max_attempts} 次,批次 ${batch})"
|
||||||
else
|
else
|
||||||
|
out_len="${#result}"
|
||||||
result="$(printf '%s' "$result" | head -c "$SLEEP_OUTPUT_LIMIT" | node "${SCRIPT_DIR}/transcript.js" redact 2>/dev/null)"
|
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)"
|
applied="$(printf '%s' "$result" | node "${SCRIPT_DIR}/memory.js" apply --role "$ROLE" 2>/dev/null)"
|
||||||
[ -n "$applied" ] && break
|
[ -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
|
fi
|
||||||
attempt=$((attempt + 1))
|
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
|
done
|
||||||
if [ -z "$applied" ]; then
|
if [ -z "$applied" ]; then
|
||||||
role_log "ERR" "整理失敗 ${max_attempts} 次(角色 ${ROLE}),保留待整理記憶到下個週期"
|
role_log "ERR" "整理失敗(角色 ${ROLE},已降到批次 ${batch}),保留待整理記憶到下個週期"
|
||||||
role_lock_release "$ROLE"
|
role_lock_release "$ROLE"
|
||||||
trap - EXIT
|
trap - EXIT
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
[ "$attempt" -gt 1 ] && role_log "INF" "第 ${attempt} 次嘗試成功(角色 ${ROLE})"
|
[ "$attempt" -gt 1 ] && role_log "INF" "第 ${attempt} 次嘗試成功(角色 ${ROLE},批次 ${batch})"
|
||||||
role_log "INF" "記憶整理完成(角色 ${ROLE},觸發:${reason}):${applied}"
|
role_log "INF" "記憶整理完成(角色 ${ROLE},觸發:${reason}):${applied}"
|
||||||
|
|
||||||
forgotten="$(node "${SCRIPT_DIR}/memory.js" forget --role "$ROLE" 2>/dev/null | tr '\n' ';')"
|
forgotten="$(node "${SCRIPT_DIR}/memory.js" forget --role "$ROLE" 2>/dev/null | tr '\n' ';')"
|
||||||
|
|||||||
@@ -786,6 +786,31 @@ updated: <yyyy/MM/dd HH:mm:ss>
|
|||||||
|
|
||||||
判準刻意放在 `relevance` 而非型態:並非所有事件與知識都該優待 —— **沒有情感脈絡的一次性工作進度仍照原規則淡去**,被留下的是帶著溫度的那些。因此兩份 prompt 都明訂:只要內容承載情感、關係溫度或當時的心情,`relevance` **必須**含 `emotional`;`episodic` 與 `semantic` 最容易漏標,漏了就會被當成一般進度處理。
|
判準刻意放在 `relevance` 而非型態:並非所有事件與知識都該優待 —— **沒有情感脈絡的一次性工作進度仍照原規則淡去**,被留下的是帶著溫度的那些。因此兩份 prompt 都明訂:只要內容承載情感、關係溫度或當時的心情,`relevance` **必須**含 `emotional`;`episodic` 與 `semantic` 最容易漏標,漏了就會被當成一般進度處理。
|
||||||
|
|
||||||
|
### 整理批次與失敗診斷
|
||||||
|
|
||||||
|
**單批則數預設 4**,並在失敗時逐次降批(4→2→1)。這個值被實測修正過三次,過程留在 `memory.js` 的註解裡,因為每一次的錯誤推論都很容易再犯:
|
||||||
|
|
||||||
|
| 推論 | 實測 |
|
||||||
|
| --- | --- |
|
||||||
|
| 輸出被 `SLEEP_OUTPUT_LIMIT` 截斷 | ❌ 輸出僅 5772~6224 字元,遠未達 8000 |
|
||||||
|
| 素材字元數過大 | ❌ 素材 8815(6 則)失敗、8940(3 則)成功 —— 字元數幾乎相同 |
|
||||||
|
| 純粹是則數問題 | ⚠️ 對一半 —— 另有一種失敗是 CLI 回傳 `Execution error` |
|
||||||
|
|
||||||
|
**限制因素是「一次要求模型輸出幾筆結構化 JSON」**,不是素材或輸出的字元量:則數越多,模型在中途寫壞引號或轉義的機率越高,而 JSON 壞一個字元整批就套用失敗。
|
||||||
|
|
||||||
|
失敗有兩類,對策不同 —— 舊的錯誤訊息只寫「無法套用」,把兩者蓋成同一句話,導致無法診斷:
|
||||||
|
|
||||||
|
| 現象 | 原因 | 對策 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 輸出極短且為 `Execution error` | CLI 偶發執行錯誤 | 重試(同批次) |
|
||||||
|
| 輸出是不完整的 JSON | 一次要求輸出太多筆 | 降批 |
|
||||||
|
|
||||||
|
因此 `--force`/cron 的失敗訊息會附上**素材大小、輸出長度、批次,以及輸出的前 200 字元**(已 redact)。沒有這一步就只能靠猜。
|
||||||
|
|
||||||
|
**素材必須結構完整**:`cmdCollect` 逐則累加、超出預算就留到下批,並替 `EXISTING` 區塊保留固定比例的預算。舊版是先組好再 `slice()` 硬切,會切在某則記憶中間、甚至把整個 `EXISTING` 區塊切掉 —— 而批次固定時每輪都收到同樣被截斷的素材,形成**死鎖**(實測連續 22 次失敗、`inbox` 從 12 累積到 23 則、角色整晚沒睡)。
|
||||||
|
|
||||||
|
**批次預設值只能有一個來源**(`memory.js` 的 `SLEEP_BATCH`)。`role_sleep.sh` 首次收集刻意不傳 `--batch`,實際則數由素材反推 —— 曾經兩邊各寫一個預設,改了 `memory.js` 卻沒生效。
|
||||||
|
|
||||||
### 繁簡防線(寫檔前的第二道)
|
### 繁簡防線(寫檔前的第二道)
|
||||||
|
|
||||||
兩份 prompt 已明令輸出繁體,但**實測仍出現整則簡體記憶**(連 `summary` 與 `tags` 都是),而且該則的 `sources` 指向另一個專案 —— 不同環境下 CLI 的行為並不一致,光靠 prompt 擋不住。因此 `memory.js` 在寫檔前再擋一次:`cmdWrite`(inbox)、`cmdApply`(整理落檔)、`appendBond`(關係史)都會經過。
|
兩份 prompt 已明令輸出繁體,但**實測仍出現整則簡體記憶**(連 `summary` 與 `tags` 都是),而且該則的 `sources` 指向另一個專案 —— 不同環境下 CLI 的行為並不一致,光靠 prompt 擋不住。因此 `memory.js` 在寫檔前再擋一次:`cmdWrite`(inbox)、`cmdApply`(整理落檔)、`appendBond`(關係史)都會經過。
|
||||||
|
|||||||
Reference in New Issue
Block a user