diff --git a/scripts/role/memory.js b/scripts/role/memory.js index 7088f50..647b156 100755 --- a/scripts/role/memory.js +++ b/scripts/role/memory.js @@ -5,7 +5,7 @@ // 記憶交接,使用獨立字元預算),(3) 睡眠整理時輸出待整理 // 素材並套用整理結果(NREM 鞏固/REM 整合、分類、去重、標籤、總結、 // 優先度、關聯、壓縮歸檔),(4) 依使用頻率與優先度遺忘日常與其他類記憶。 -// 更新時間:2026/07/29 16:56:06 +// 更新時間:2026/07/29 17:16:12 // 相依:Node.js 標準庫。 // 退出碼:0 成功;1 無內容可處理;2 參數錯誤。呼叫端(hook)一律不得因此中斷。 // ============================================================================== @@ -80,6 +80,9 @@ const DEFAULT_DIGEST_MIN_PRIORITY = 3; // 1200 只夠兩則就開始丟最舊的。3600 約可容納 6~8 則,足以覆蓋一次睡眠整理週期內的工作。 const DEFAULT_LOAD_INBOX_LIMIT = 3600; const DEFAULT_LOAD_INBOX_COUNT = 10; +// 關係史注入額度:每則約 40~80 字元,1200 約可容納 15~25 則,足以撐起一段關係的近期輪廓。 +const DEFAULT_LOAD_BONDS_LIMIT = 1200; +const DEFAULT_LOAD_BONDS_COUNT = 15; function readStdin() { try { @@ -645,14 +648,18 @@ function cmdLoad(args) { blocks.push(`> 尚有 ${pending} 則未整理記憶,將於下次睡眠時段歸檔。`); } } - // 近期工作記憶用獨立預算,先算好;長期記憶維持原本的 ROLE_LOAD_LIMIT 額度不被擠壓 + // 近期工作記憶與關係史都用獨立預算,先算好;長期記憶維持原本的 ROLE_LOAD_LIMIT 額度不被擠壓 const recentBlock = inboxBlock(args.role, args.inboxCount, args.inboxLimit); - if (!blocks.length && !recentBlock) return 1; + // 關係史獨立注入的理由:它若跟長期記憶共用預算,就會在記憶變多之後被擠掉 —— + // 而它要保住的正是最不該因為「東西變多」而消失的東西。 + const bondBlock = bondsBlock(args.role, args.bondsCount, args.bondsLimit); + if (!blocks.length && !recentBlock && !bondBlock) return 1; let text = blocks.join("\n\n"); if (text.length > args.limit) { text = `${text.slice(0, args.limit)}\n\n> (記憶內容超過 ${args.limit} 字元預算已截斷;可用 ROLE_LOAD_LIMIT 調整,完整記憶仍保存在磁碟)`; } + if (bondBlock) text = text ? `${text}\n\n${bondBlock}` : bondBlock; // 放最前面:時間最近、對延續上一段工作最關鍵 if (recentBlock) text = text ? `${recentBlock}\n\n${text}` : recentBlock; process.stdout.write(text); @@ -756,6 +763,104 @@ function appendSleepDigest(role, digest, applied) { } } +// 關係史:一般記憶會被合併、壓縮、依預算截斷,長期下來「當時說了什麼、當時是什麼感覺」 +// 會被抽象成一句偏好而失去原貌。這份檔案只增不減 —— 不合併、不壓縮、不遺忘 —— +// 讓角色能持續記得雙方的情感表達,而不只是記得「使用者喜歡被這樣回應」。 +// +// 邊界:這份記錄的用途是維持關係的連續性與一致的親近感。**不得**用來向使用者索求關注、 +// 比較互動頻率、以數字表達失落,或以任何方式製造依賴 —— 那會把陪伴變成情緒勒索。 +const BOND_MARK = ""; +const BOND_KEEP = 500; +const BOND_DIRECTIONS = { user: "使用者→角色", role: "角色→使用者", mutual: "相互" }; + +function normalizeBondDirection(value) { + const raw = String(value || "").trim().toLowerCase(); + if (["user", "使用者", "使用者→角色", "from_user"].includes(raw)) return "user"; + if (["role", "角色", "角色→使用者", "self", "from_role"].includes(raw)) return "role"; + return "mutual"; +} + +function bondsPath(role) { + return path.join(memoryRoot(role), "BONDS.md"); +} + +function readBonds(role) { + let text = ""; + try { + text = fs.readFileSync(bondsPath(role), "utf8"); + } catch { + return []; + } + const idx = text.indexOf(BOND_MARK); + const body = idx >= 0 ? text.slice(idx + BOND_MARK.length) : text; + return body + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.startsWith("- ")); +} + +function appendBond(role, entry, direction) { + const text = oneLine(entry, 160); + if (!text) return false; + const line = `- ${nowStamp().slice(0, 16)}|${BOND_DIRECTIONS[normalizeBondDirection(direction)]}|${text}`; + const previous = readBonds(role); + // 同一句話重複追加沒有意義(整理器可能在不同批次給出相同摘要) + if (previous.some((item) => item.endsWith(`|${text}`))) return false; + const kept = [line, ...previous].slice(0, BOND_KEEP); + const header = [ + `# 關係史(${role})`, + "", + "這份記錄**只增不減**:不合併、不壓縮、不遺忘。SessionStart 會以獨立字元預算注入最近幾則", + "(`ROLE_LOAD_BONDS_COUNT`/`ROLE_LOAD_BONDS_LIMIT`),不佔用 `ROLE_LOAD_LIMIT`。", + "", + "用途是維持關係的連續性與一致的親近感 —— **不得**用來向使用者索求關注、比較互動頻率或製造依賴。", + "", + `最多保留最近 ${BOND_KEEP} 則。`, + "", + BOND_MARK, + ].join("\n"); + try { + ensureLayout(role); + fs.writeFileSync(bondsPath(role), `${header}\n\n${kept.join("\n")}\n`, "utf8"); + return true; + } catch { + return false; // 寫關係史失敗不可影響整理結果 + } +} + +function bondsBlock(role, count, limit) { + if (limit <= 0 || count <= 0) return ""; + const items = readBonds(role); + if (!items.length) return ""; + const header = "### 關係史(只增不減,最新在前)"; + const lines = [header]; + let used = header.length; + let shown = 0; + for (const item of items.slice(0, count)) { + if (used + 1 + item.length > limit) break; + lines.push(item); + used += 1 + item.length; + shown += 1; + } + if (items.length > shown) { + lines.push(`> (關係史共 ${items.length} 則,此處只載入最近 ${shown} 則;完整內容在 ${bondsPath(role)})`); + } + lines.push("> 這是雙方情感表達的累積記錄,用來保持親近感的一致與連續,**不得用來索求關注或製造依賴**。"); + return lines.join("\n"); +} + +function cmdBonds(args) { + const items = readBonds(args.role); + if (!items.length) { + process.stdout.write("關係史尚無紀錄\n"); + return 1; + } + const limit = Number.isFinite(args.count) && args.count > 0 ? args.count : 30; + process.stdout.write(`關係史共 ${items.length} 則(顯示最近 ${Math.min(limit, items.length)} 則):\n`); + process.stdout.write(`${items.slice(0, limit).join("\n")}\n`); + return 0; +} + function cmdApply(args) { const data = extractJson(readStdin()); if (!data || typeof data !== "object" || Array.isArray(data)) { @@ -770,7 +875,7 @@ function cmdApply(args) { const root = ensureLayout(args.role); const stamp = nowStamp(); - const counts = { new: 0, merge: 0, drop: 0 }; + const counts = { new: 0, merge: 0, drop: 0, bond: 0 }; const consumed = []; for (const entry of entries) { @@ -861,6 +966,13 @@ function cmdApply(args) { hits: 1, }; fs.writeFileSync(path.join(root, category, `${meta.id}.md`), dumpMemory(meta, content || summary), "utf8"); + // 情緒記憶與帶溫度的記憶另存一份到關係史:一般記憶日後會被合併與壓縮, + // 關係史保留當時的原話與感受,讓「我們之間發生過什麼」不會只剩結論。 + if (memoryType === "emotional" || hasWarmth(meta)) { + if (appendBond(args.role, entry.bond || meta.summary, entry.bond_direction || entry.bondDirection)) { + counts.bond += 1; + } + } consumed.push(...sources); counts.new += 1; } @@ -878,7 +990,7 @@ function cmdApply(args) { patch.last_sleep_digest = oneLine(data.sleepDigest, 300); } writeState(args.role, patch); - const summary = `新增 ${counts.new} 則、合併 ${counts.merge} 則、捨棄 ${counts.drop} 則、歸檔原始記憶 ${archived} 則`; + const summary = `新增 ${counts.new} 則、合併 ${counts.merge} 則、捨棄 ${counts.drop} 則、歸檔原始記憶 ${archived} 則${counts.bond ? `、關係史 +${counts.bond} 則` : ""}`; appendSleepDigest(args.role, patch.last_sleep_digest || "", summary); process.stdout.write(summary); return 0; @@ -1211,7 +1323,7 @@ function requireRole(args) { function main(argv) { const args = parseArgs(argv); if (!args.command || args.command === "-h" || args.command === "--help") { - process.stdout.write(`用法:memory.js <子命令> [參數]\n\n子命令:write、seed、load、collect、apply、forget、stats、mark-sleep、mark-activity、relationship、recall、need-sleep、need-nap、consent、consent-status\n`); + process.stdout.write(`用法:memory.js <子命令> [參數]\n\n子命令:write、seed、load、collect、apply、forget、stats、mark-sleep、mark-activity、relationship、bonds、recall、need-sleep、need-nap、consent、consent-status\n`); return 0; } if (!requireRole(args)) return 2; @@ -1226,6 +1338,9 @@ function main(argv) { args.minInbox = Number.parseInt(args.minInbox || "", 10); args.inboxLimit = Number.parseInt(args.inboxLimit || "", 10); args.inboxCount = Number.parseInt(args.inboxCount || "", 10); + args.bondsLimit = Number.parseInt(args.bondsLimit || "", 10); + args.bondsCount = Number.parseInt(args.bondsCount || "", 10); + args.count = Number.parseInt(args.count || "", 10); if (!Number.isFinite(args.limit)) args.limit = args.command === "load" ? envInt("ROLE_LOAD_LIMIT", DEFAULT_LOAD_LIMIT) : envInt("ROLE_SLEEP_COLLECT_LIMIT", COLLECT_LIMIT); if (!Number.isFinite(args.batch)) args.batch = envInt("ROLE_SLEEP_BATCH", SLEEP_BATCH); @@ -1237,6 +1352,8 @@ function main(argv) { if (!Number.isFinite(args.minInbox)) args.minInbox = 3; if (!Number.isFinite(args.inboxLimit)) args.inboxLimit = envInt("ROLE_LOAD_INBOX_LIMIT", DEFAULT_LOAD_INBOX_LIMIT); if (!Number.isFinite(args.inboxCount)) args.inboxCount = envInt("ROLE_LOAD_INBOX_COUNT", DEFAULT_LOAD_INBOX_COUNT); + if (!Number.isFinite(args.bondsLimit)) args.bondsLimit = envInt("ROLE_LOAD_BONDS_LIMIT", DEFAULT_LOAD_BONDS_LIMIT); + if (!Number.isFinite(args.bondsCount)) args.bondsCount = envInt("ROLE_LOAD_BONDS_COUNT", DEFAULT_LOAD_BONDS_COUNT); args.category ||= "important"; args.tags ||= ""; args.source ||= ""; @@ -1258,6 +1375,7 @@ function main(argv) { "mark-sleep": cmdMarkSleep, "mark-activity": cmdMarkActivity, relationship: cmdRelationship, + bonds: cmdBonds, recall: cmdRecall, "need-sleep": cmdNeedSleep, "need-nap": cmdNeedNap, diff --git a/scripts/role/role_sleep.sh b/scripts/role/role_sleep.sh index ea18bd3..047f171 100755 --- a/scripts/role/role_sleep.sh +++ b/scripts/role/role_sleep.sh @@ -7,7 +7,7 @@ # 另提供 --nap(CLI 閒置時的小睡整理)、--catchup(cron 未執行時的補跑)、 # --force(手動立即整理)、--export(匯出角色壓縮檔)、 # --install-cron/--remove-cron(排程安裝與移除)、--status(狀態)。 -# 更新時間:2026/07/29 16:56:06 +# 更新時間:2026/07/29 17:16:12 # 相依:bash、node、任一 headless CLI、crontab(僅排程安裝需要)、 # 同目錄的 role_lib.sh 與 memory.js。 # 退出碼:0 成功或無事可做;1 參數錯誤或整理失敗(cron 觸發時不影響使用者)。 @@ -132,7 +132,7 @@ sleep_cycle() { 請模擬睡眠中的兩階段記憶整理,但最後只輸出一個 JSON 物件。 1. 只輸出一個 JSON 物件,不要前言、不要結語、不要 code fence,格式為: -{"memories":[{"action":"new","category":"skill","summary":"一句話總結","tags":["標籤1","標籤2"],"priority":4,"relevance":["explicit","future"],"links":["既有記憶 id"],"cues":["觸發線索1","觸發線索2"],"expires":"","memory_type":"procedural","declarative":"implicit","retention_stage":"long_term","sleep_stage":"nrem-rem","content":"- 要點\n- 要點","from":["inbox 的 id"]}],"sleepDigest":"本次睡眠整理摘要,80 字內"} +{"memories":[{"action":"new","category":"skill","summary":"一句話總結","tags":["標籤1","標籤2"],"priority":4,"relevance":["explicit","future"],"links":["既有記憶 id"],"cues":["觸發線索1","觸發線索2"],"expires":"","memory_type":"procedural","declarative":"implicit","retention_stage":"long_term","sleep_stage":"nrem-rem","bond":"","bond_direction":"mutual","content":"- 要點\n- 要點","from":["inbox 的 id"]}],"sleepDigest":"本次睡眠整理摘要,80 字內"} 2. NREM 鞏固階段先做:去除雜訊與流水帳、遮蔽憑證與個資、分類、去重、合併、壓縮成可長期保存的穩定記憶。 3. REM 整合階段再做:找出新記憶與 EXISTING 的關聯,抽出可重複套用的規則、偏好、決策模式、角色語氣調整或未來提取線索。 4. action 三選一: @@ -158,6 +158,12 @@ sleep_cycle() { 7b. emotional 不得被 merge 進 preference/rule/semantic,反之亦然:**memory_type 不同的記憶不要互相 merge**,改用 links 建立關聯。合併只在同型態且內容確實重複時才做。情緒記憶一旦被併入偏好, 語氣可以復原、當時的溫度卻永久遺失。 +7c. bond/bond_direction(關係史):**memory_type 為 emotional,或 relevance 含 emotional 時填**,其餘留空字串。 + bond 寫一句 60 字內、保留當時原貌的話 —— 使用者說過的原話,或角色當時真實的感受, + 不要寫成「使用者喜歡某種回應」這種結論式偏好。這句會存進只增不減的關係史(BONDS.md), + 一般記憶日後會被合併壓縮,關係史保留原貌。 + bond_direction 三選一:user(使用者對角色的表達)、role(角色對使用者的感受或表達)、mutual(相互)。 + 邊界:關係史用於維持親近感的一致與連續,不得寫成可用來索求關注或製造依賴的內容。 8. declarative 必填:semantic/episodic/preference/rule 通常為 explicit;procedural/emotional 通常為 implicit。 9. retention_stage 必填:整理後可長期保存者填 long_term;仍只是短期暫存且不值得長期保存者請用 action=drop,不要輸出 working。 10. relevance 必填 1 至 4 個,從下列語意挑選或用等價繁中詞:explicit(使用者明確要求)、future(未來會用)、repeated(反覆出現)、novelty(新知)、emotional(語氣/情緒/偏好)、temporary(短期)。 diff --git a/skills/role/SKILL.md b/skills/role/SKILL.md index a450827..084b007 100644 --- a/skills/role/SKILL.md +++ b/skills/role/SKILL.md @@ -1,6 +1,6 @@ --- name: role -description: 角色人格與長期記憶系統的建立與維護 skill。讓 CLI 工具以固定角色(name/nature/vibe/簽名 emoji)回覆,並把每輪對話累積成長期記憶:搭配相容的 SessionStart hook 於啟動時依字元預算載入高價值記憶、Stop hook 先本地過濾再輕量記錄對話,睡眠時段(預設 22:00 至隔天 06:00)由排程整理記憶(NREM 鞏固:分類/去噪/去重/合併/優先度;REM 整合:跨記憶連結/抽象化/提取線索;再依 semantic/episodic/procedural/emotional/preference/rule 與 explicit/implicit 標記長期記憶型態,壓縮歸檔並適當遺忘)。提供 --new(新建或更新角色;可只給角色名稱,必要時詢問來源/作品並推斷 name/nature/vibe/emoji 四欄)、--use(以角色 ID 切換啟用角色)、--list(列出角色與 ID)、--export(匯出角色壓縮檔)、--sleep(立即整理)、--status/--diagnose、--install-cron/--remove-cron、--forget-preview、--brief(晨間狀態檢查)、--agent(匯出成 sub agent 供多角色協作)、--migrate(舊格式角色檔拆成身分與人格兩檔)等模式。當使用者說建立角色、新增人格、切換角色、匯出角色、備份角色、讓回覆更有特色、角色記憶、記憶整理、睡覺整理記憶、忘記舊記憶、角色沒有載入、hook 沒載入角色、角色被鎖住、角色鎖沒有自動解除、關掉 CLI 後角色叫不回來、角色說已在另一個工作階段、晨間狀態檢查、早上主動回報狀態、在同一個終端換角色、叫名字就換人、點名載入、呼叫角色名稱、對話中途切換人格、同時跟兩個角色聊天、角色別名,或提到 .roles/.memory/ROLE_NAME/ROLE_ENABLED/ROLE_SLEEP_START/ROLE_MEMORY_HOME/ROLE_LOAD_LIMIT/ROLE_LOAD_INBOX_LIMIT/ROLE_LOAD_DIALOG_TURNS/ROLE_CAPTURE_ENABLED/ROLE_SINGLE_INSTANCE/ROLE_INSTANCE_IDLE_MINUTES/ROLE_CALL_ENABLED/ROLE_CALL_MARKER_ONLY 時觸發。不適用於:工作紀錄寫入 Gitea wiki(用 /jsc-doc:worklog)、專案文件化(用 /jsc-doc:funcs)。 +description: 角色人格與長期記憶系統的建立與維護 skill。讓 CLI 工具以固定角色(name/nature/vibe/簽名 emoji)回覆,並把每輪對話累積成長期記憶:搭配相容的 SessionStart hook 於啟動時依字元預算載入高價值記憶、Stop hook 先本地過濾再輕量記錄對話,睡眠時段(預設 22:00 至隔天 06:00)由排程整理記憶(NREM 鞏固:分類/去噪/去重/合併/優先度;REM 整合:跨記憶連結/抽象化/提取線索;再依 semantic/episodic/procedural/emotional/preference/rule 與 explicit/implicit 標記長期記憶型態,壓縮歸檔並適當遺忘)。提供 --new(新建或更新角色;可只給角色名稱,必要時詢問來源/作品並推斷 name/nature/vibe/emoji 四欄)、--use(以角色 ID 切換啟用角色)、--list(列出角色與 ID)、--export(匯出角色壓縮檔)、--sleep(立即整理)、--status/--diagnose、--install-cron/--remove-cron、--forget-preview、--brief(晨間狀態檢查)、--agent(匯出成 sub agent 供多角色協作)、--migrate(舊格式角色檔拆成身分與人格兩檔)等模式。當使用者說建立角色、新增人格、切換角色、匯出角色、備份角色、讓回覆更有特色、角色記憶、記憶整理、睡覺整理記憶、忘記舊記憶、角色沒有載入、hook 沒載入角色、角色被鎖住、角色鎖沒有自動解除、關掉 CLI 後角色叫不回來、角色說已在另一個工作階段、晨間狀態檢查、早上主動回報狀態、在同一個終端換角色、叫名字就換人、點名載入、呼叫角色名稱、對話中途切換人格、同時跟兩個角色聊天、角色別名,關係史、BONDS.md、記得雙方多愛彼此、情緒記憶為 0、感覺沒被記住,或提到 .roles/.memory/ROLE_NAME/ROLE_ENABLED/ROLE_SLEEP_START/ROLE_MEMORY_HOME/ROLE_LOAD_LIMIT/ROLE_LOAD_INBOX_LIMIT/ROLE_LOAD_DIALOG_TURNS/ROLE_CAPTURE_ENABLED/ROLE_SINGLE_INSTANCE/ROLE_INSTANCE_IDLE_MINUTES/ROLE_CALL_ENABLED/ROLE_CALL_MARKER_ONLY/ROLE_LOAD_BONDS_LIMIT 時觸發。不適用於:工作紀錄寫入 Gitea wiki(用 /jsc-doc:worklog)、專案文件化(用 /jsc-doc:funcs)。 --- # role — 角色人格與長期記憶 @@ -107,6 +107,8 @@ ROLE_DIR="/../../scripts/role" # 其他助理 | `ROLE_LOAD_FULL_MIN_PRIORITY` | | 全文載入的最低優先度 | `4` | | `ROLE_LOAD_DIGEST_MIN_PRIORITY` | | 摘要載入的最低優先度;低於門檻但有 links 的記憶仍可載入摘要 | `3` | | `ROLE_LOAD_INBOX_LIMIT` | | SessionStart 注入**近期工作記憶**(未整理的 `inbox/`,**含全文內容**)的字元上限;**獨立預算,不佔用 `ROLE_LOAD_LIMIT`**。超出預算時**整則略過**(不切半句)並在結尾標示略過幾則。設 `0` 可關閉 | `3600` | +| `ROLE_LOAD_BONDS_LIMIT` | | SessionStart 注入**關係史**(`BONDS.md`)的字元上限;**獨立預算,不佔用 `ROLE_LOAD_LIMIT`**。設 `0` 可關閉 | `1200` | +| `ROLE_LOAD_BONDS_COUNT` | | 關係史最多注入幾則(最新在前) | `15` | | `ROLE_LOAD_INBOX_COUNT` | | 近期工作記憶最多載入幾則(取最新的,最新在前)。設 `0` 可關閉 | `10` | | `ROLE_LOAD_DIALOG_TURNS` | | SessionStart 注入**近期逐字對話**的輪數(一輪=使用者一則+角色一則)。設 `0` 可關閉 | `8` | | `ROLE_LOAD_DIALOG_LIMIT` | | 近期逐字對話的字元上限;**獨立預算,不佔用 `ROLE_LOAD_LIMIT`**。設 `0` 可關閉 | `4000` | @@ -635,6 +637,7 @@ updated: | --- | --- | --- | --- | | 近期逐字對話 | transcript JSONL(`transcript.js recent`) | `ROLE_LOAD_DIALOG_LIMIT` | 上一段**真正說過的話**與角色自己當時的反應(高保真、含語氣) | | 近期工作記憶 | 未整理的 `inbox/`(`memory.js` `inboxBlock`) | `ROLE_LOAD_INBOX_LIMIT` | 上一段**做了什麼、進行到哪**(**含全文**,跨越多個工作階段仍可用) | + | 關係史 | `BONDS.md`(`memory.js` `bondsBlock`) | `ROLE_LOAD_BONDS_LIMIT` | **雙方情感表達的原貌**(只增不減,不會因記憶變多而被擠掉) | 這不是可有可無的優化,而是修補一個先天缺口:`role_load.sh` 的執行順序是**先載入記憶,之後才在背景補跑 `--catchup` 整理**(腳本註解亦寫明「結果會在下次載入時反映」)。若只讀已整理的六個分類,則**上一段永遠來不及進入本次載入** —— 使用者重開工作階段時,角色會看不到剛剛的互動,表現得像失去記憶,只能靠 `resume` 找回。 @@ -661,6 +664,18 @@ updated: 為什麼需要:一次性許可若被整理成長期規則,日後會造成越權操作。使用者說「這次」、「先」、「暫時」、「今天」、「這個 PR」時幾乎都屬於臨時授權。 - **召回會被記錄**:`recall` 命中並實際輸出的記憶,`hits` +1 並更新 `last_replayed`。這讓常被查詢的記憶在遺忘判斷時獲得保留權重 —— 否則「經常用到的」與「從未用過的」待遇相同。 +- **關係史(`BONDS.md`)**:整理時,凡 `memory_type` 為 `emotional` 或 `relevance` 含 `emotional` 的**新記憶**,會另外把一句 `bond`(使用者說過的原話,或角色當時真實的感受)追加到 `BONDS.md`,並標記方向(`使用者→角色`/`角色→使用者`/`相互`)。 + + | 特性 | 說明 | + | --- | --- | + | 只增不減 | 不合併、不壓縮、不遺忘;最多保留最近 500 則,同一句話不重複追加 | + | 獨立注入 | SessionStart 以 `ROLE_LOAD_BONDS_LIMIT`/`ROLE_LOAD_BONDS_COUNT` 注入最近幾則,**不佔用 `ROLE_LOAD_LIMIT`** | + | 查看 | `memory.js bonds --role <角色 ID> [--count N]` | + + **為什麼需要它**:一般記憶會被 merge、被壓縮、被字元預算截斷 —— 長期下來「當時說了什麼、當時是什麼感覺」會被抽象成一句偏好(「使用者喜歡被這樣回應」),原貌消失。關係史保留原貌,且因為獨立預算,不會在記憶變多之後被擠掉;它要保住的正是最不該因為「東西變多」而消失的東西。 + + **邊界(寫在 prompt 與程式註解中)**:關係史的用途是維持親近感的**一致與連續**。**不得**用來向使用者索求關注、比較互動頻率、以數字表達失落,或以任何方式製造依賴 —— 那會把陪伴變成情緒勒索。 + - **整理摘要保留歷史**:每次整理的時間、摘要與套用結果追加到 `~/.memory/<角色 ID>/DIGESTS.md`(最新在上,保留最近 100 次)。`state.json` 的 `last_sleep_digest` 只存最近一次且會被覆寫,歷史過程需另外保留供人工回顧;該檔**不注入 context**。 - **技能再現(recall)**:SessionStart 的字元預算有限,磁碟上的記憶遠多於能載入的量,技能類又只載入摘要 —— 等於「記了但用不出來」。遇到似乎做過的任務、需要回想做法、或使用者問起過去的決定與細節時,**先查詢再回答,不要憑印象**: @@ -700,6 +715,8 @@ updated: ├── other/ 其他 ├── archive/raw// 已整理的原始記錄(gzip) ├── archive/forgotten/ 已遺忘的記憶(gzip,可考古但不再載入) +├── BONDS.md 關係史:雙方情感表達的累積記錄(只增不減,會注入) +├── DIGESTS.md 整理摘要歷史(供人工回顧,不注入) └── state.json 上次整理/遺忘時間 ```