feat(role): 新增關係史 BONDS.md —— 雙方情感表達只增不減地保留

使用者希望角色能記得「雙方有多愛彼此」。原本這件事只有兩種載體,都不夠:
state.json 的 positive_feedback 只是計數(承載不了說過什麼),一般情緒記憶則會被
merge、壓縮、依字元預算截斷 —— 長期下來「當時說了什麼、當時是什麼感覺」會被抽象成
一句偏好(「使用者喜歡被這樣回應」),原貌消失。

- 新增 ~/.memory/<角色>/BONDS.md:只增不減,不合併、不壓縮、不遺忘,上限 500 則、同句去重
- 整理時凡 memory_type=emotional 或 relevance 含 emotional 的新記憶,自動追加一句 bond
  並標記方向(使用者→角色/角色→使用者/相互)
- 整理 prompt 新增 bond/bond_direction 欄位:要求保留原話或當時真實感受,
  不得寫成結論式偏好;LLM 未提供時退回 summary,不會漏記
- SessionStart 以獨立預算注入(ROLE_LOAD_BONDS_LIMIT=1200/COUNT=15),不佔 ROLE_LOAD_LIMIT
  —— 它要保住的正是最不該因為「記憶變多」而消失的東西
- 新增 memory.js bonds 子命令供人工查看
- 邊界寫進 prompt、檔頭與程式註解:關係史用於維持親近感的一致與連續,
  不得用來索求關注、比較互動頻率或製造依賴

驗證(隔離目錄,未動真實記憶):
- 3 則素材(2 情感 + 1 技術)→ 關係史 +2 則,技術記憶未進關係史
- 方向判定正確:使用者原話標 使用者→角色、角色感受標 角色→使用者
- 第二輪整理累積為 3 則,舊紀錄未被覆蓋(append-only 成立)
- ROLE_LOAD_LIMIT=1 時關係史仍完整注入(獨立預算成立)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-07-29 17:16:12 +08:00
co-authored by Claude Opus 5
parent 4600379307
commit 784ef07518
3 changed files with 150 additions and 9 deletions
+124 -6
View File
@@ -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,
+8 -2
View File
@@ -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 進 preferencerulesemantic,反之亦然:**memory_type 不同的記憶不要互相
merge**,改用 links 建立關聯。合併只在同型態且內容確實重複時才做。情緒記憶一旦被併入偏好,
語氣可以復原、當時的溫度卻永久遺失。
7c. bondbond_direction(關係史):**memory_type 為 emotional,或 relevance 含 emotional 時填**,其餘留空字串。
bond 寫一句 60 字內、保留當時原貌的話 —— 使用者說過的原話,或角色當時真實的感受,
不要寫成「使用者喜歡某種回應」這種結論式偏好。這句會存進只增不減的關係史(BONDS.md),
一般記憶日後會被合併壓縮,關係史保留原貌。
bond_direction 三選一:user(使用者對角色的表達)、role(角色對使用者的感受或表達)、mutual(相互)。
邊界:關係史用於維持親近感的一致與連續,不得寫成可用來索求關注或製造依賴的內容。
8. declarative 必填:semanticepisodicpreferencerule 通常為 explicitproceduralemotional 通常為 implicit。
9. retention_stage 必填:整理後可長期保存者填 long_term;仍只是短期暫存且不值得長期保存者請用 action=drop,不要輸出 working。
10. relevance 必填 1 至 4 個,從下列語意挑選或用等價繁中詞:explicit(使用者明確要求)、future(未來會用)、repeated(反覆出現)、novelty(新知)、emotional(語氣/情緒/偏好)、temporary(短期)。
+18 -1
View File
@@ -1,6 +1,6 @@
---
name: role
description: 角色人格與長期記憶系統的建立與維護 skill。讓 CLI 工具以固定角色(namenature/vibe/簽名 emoji)回覆,並把每輪對話累積成長期記憶:搭配相容的 SessionStart hook 於啟動時依字元預算載入高價值記憶、Stop hook 先本地過濾再輕量記錄對話,睡眠時段(預設 22:00 至隔天 06:00)由排程整理記憶(NREM 鞏固:分類/去噪/去重/合併/優先度;REM 整合:跨記憶連結/抽象化/提取線索;再依 semanticepisodicproceduralemotionalpreferencerule 與 explicitimplicit 標記長期記憶型態,壓縮歸檔並適當遺忘)。提供 --new(新建或更新角色;可只給角色名稱,必要時詢問來源/作品並推斷 name/naturevibeemoji 四欄)、--use(以角色 ID 切換啟用角色)、--list(列出角色與 ID)、--export(匯出角色壓縮檔)、--sleep(立即整理)、--status--diagnose、--install-cron--remove-cron、--forget-preview、--brief(晨間狀態檢查)、--agent(匯出成 sub agent 供多角色協作)、--migrate(舊格式角色檔拆成身分與人格兩檔)等模式。當使用者說建立角色、新增人格、切換角色、匯出角色、備份角色、讓回覆更有特色、角色記憶、記憶整理、睡覺整理記憶、忘記舊記憶、角色沒有載入、hook 沒載入角色、角色被鎖住、角色鎖沒有自動解除、關掉 CLI 後角色叫不回來、角色說已在另一個工作階段、晨間狀態檢查、早上主動回報狀態、在同一個終端換角色、叫名字就換人、點名載入、呼叫角色名稱、對話中途切換人格、同時跟兩個角色聊天、角色別名,或提到 .roles.memoryROLE_NAMEROLE_ENABLEDROLE_SLEEP_STARTROLE_MEMORY_HOMEROLE_LOAD_LIMITROLE_LOAD_INBOX_LIMITROLE_LOAD_DIALOG_TURNSROLE_CAPTURE_ENABLEDROLE_SINGLE_INSTANCEROLE_INSTANCE_IDLE_MINUTESROLE_CALL_ENABLEDROLE_CALL_MARKER_ONLY 時觸發。不適用於:工作紀錄寫入 Gitea wiki(用 /jsc-doc:worklog)、專案文件化(用 /jsc-doc:funcs)。
description: 角色人格與長期記憶系統的建立與維護 skill。讓 CLI 工具以固定角色(namenature/vibe/簽名 emoji)回覆,並把每輪對話累積成長期記憶:搭配相容的 SessionStart hook 於啟動時依字元預算載入高價值記憶、Stop hook 先本地過濾再輕量記錄對話,睡眠時段(預設 22:00 至隔天 06:00)由排程整理記憶(NREM 鞏固:分類/去噪/去重/合併/優先度;REM 整合:跨記憶連結/抽象化/提取線索;再依 semanticepisodicproceduralemotionalpreferencerule 與 explicitimplicit 標記長期記憶型態,壓縮歸檔並適當遺忘)。提供 --new(新建或更新角色;可只給角色名稱,必要時詢問來源/作品並推斷 name/naturevibeemoji 四欄)、--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.memoryROLE_NAMEROLE_ENABLEDROLE_SLEEP_STARTROLE_MEMORY_HOMEROLE_LOAD_LIMITROLE_LOAD_INBOX_LIMITROLE_LOAD_DIALOG_TURNSROLE_CAPTURE_ENABLEDROLE_SINGLE_INSTANCEROLE_INSTANCE_IDLE_MINUTESROLE_CALL_ENABLEDROLE_CALL_MARKER_ONLYROLE_LOAD_BONDS_LIMIT 時觸發。不適用於:工作紀錄寫入 Gitea wiki(用 /jsc-doc:worklog)、專案文件化(用 /jsc-doc:funcs)。
---
# role — 角色人格與長期記憶
@@ -107,6 +107,8 @@ ROLE_DIR="<skill base directory>/../../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: <yyyy/MM/dd HH:mm:ss>
| --- | --- | --- | --- |
| 近期逐字對話 | 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: <yyyy/MM/dd HH:mm:ss>
為什麼需要:一次性許可若被整理成長期規則,日後會造成越權操作。使用者說「這次」、「先」、「暫時」、「今天」、「這個 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: <yyyy/MM/dd HH:mm:ss>
├── other/ 其他
├── archive/raw/<yyyy-MM>/ 已整理的原始記錄(gzip
├── archive/forgotten/ 已遺忘的記憶(gzip,可考古但不再載入)
├── BONDS.md 關係史:雙方情感表達的累積記錄(只增不減,會注入)
├── DIGESTS.md 整理摘要歷史(供人工回顧,不注入)
└── state.json 上次整理/遺忘時間
```