feat(role): 記得溫度 —— 情緒記憶修復 + 有溫度的記憶優先 + 關係史 BONDS.md(0.1.0) #24
+124
-6
@@ -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,
|
||||
|
||||
@@ -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(短期)。
|
||||
|
||||
+18
-1
@@ -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="<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 上次整理/遺忘時間
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user