feat(role): SessionStart 載入未整理的近期工作記憶
修補記憶交接的先天缺口:role_load.sh 的順序是先 memory.js load 載入記憶, 之後才在背景補跑 --catchup 整理(腳本註解亦寫明「結果會在下次載入時反映」)。 而 load 原本只讀已整理的六個分類,完全不讀 inbox,導致上一段工作永遠來不及 進入本次載入 —— 使用者重開工作階段時,角色看不到剛剛做過的事,表現得像失去 記憶,只能靠 resume 找回上下文。 - memory.js 新增 inboxBlock():讀 inbox 最新數則的 summary,附時間與標籤,最新在前 - cmdLoad 把該區塊放在最前面,並使用獨立字元預算,不佔用 ROLE_LOAD_LIMIT (已驗證:開關 inbox 時長期記憶區塊逐字元完全一致,未被擠壓) - 新增 ROLE_LOAD_INBOX_LIMIT(預設 1200)與 ROLE_LOAD_INBOX_COUNT(預設 10), 任一設 0 可關閉 - role skill 補「近期工作記憶交接(不可移除)」段落,寫明缺口原因與範圍限制: 這是摘要級交接,角色會知道上一段在做什麼、進行到哪,但不等於逐字記得整段 對話;需要完整上下文仍應使用 resume - 版號 0.0.1 → 0.0.2 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+35
-2
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
// ==============================================================================
|
||||
// 用途:角色記憶(.memory/<角色>/)的儲存引擎。負責 (1) 把每輪對話濃縮結果寫入
|
||||
// inbox,(2) 產生 SessionStart 要注入的記憶區塊,(3) 睡眠整理時輸出待整理
|
||||
// inbox,(2) 產生 SessionStart 要注入的記憶區塊(含未整理 inbox 的近期工作
|
||||
// 記憶交接,使用獨立字元預算),(3) 睡眠整理時輸出待整理
|
||||
// 素材並套用整理結果(NREM 鞏固/REM 整合、分類、去重、標籤、總結、
|
||||
// 優先度、關聯、壓縮歸檔),(4) 依使用頻率與優先度遺忘日常與其他類記憶。
|
||||
// 更新時間:2026/07/28 14:36:00
|
||||
@@ -64,6 +65,8 @@ const CONTENT_LIMIT = 1200;
|
||||
const DEFAULT_LOAD_LIMIT = 4000;
|
||||
const DEFAULT_FULL_MIN_PRIORITY = 4;
|
||||
const DEFAULT_DIGEST_MIN_PRIORITY = 3;
|
||||
const DEFAULT_LOAD_INBOX_LIMIT = 1200;
|
||||
const DEFAULT_LOAD_INBOX_COUNT = 10;
|
||||
|
||||
function readStdin() {
|
||||
try {
|
||||
@@ -364,6 +367,28 @@ function listInbox(role) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// 近期工作記憶區塊:SessionStart 載入尚未整理的 inbox 摘要。
|
||||
// inbox 是「剛剛發生的事」,但整理(NREM/REM)永遠跑在載入之後(role_load.sh 先 load 再背景 catchup),
|
||||
// 若不在此載入,重開工作階段時角色會看不到上一段工作,表現得像失去記憶。
|
||||
// 使用獨立字元預算,不佔用長期記憶的 ROLE_LOAD_LIMIT。
|
||||
function inboxBlock(role, count, limit) {
|
||||
if (limit <= 0 || count <= 0) return "";
|
||||
const items = listInbox(role);
|
||||
if (!items.length) return "";
|
||||
const recent = items.slice(-count).reverse(); // 檔名為時間戳,取最後 N 則後反轉成最新在前
|
||||
const lines = ["### 近期工作記憶(未整理,最新在前)"];
|
||||
for (const [meta] of recent) {
|
||||
const when = typeof meta.created === "string" && meta.created.length >= 16 ? meta.created.slice(11, 16) : "--:--";
|
||||
const tags = (meta.tags || []).join("、");
|
||||
lines.push(`- ${when} ${meta.summary || "(無總結)"}${tags ? `(${tags})` : ""}`);
|
||||
}
|
||||
let text = lines.join("\n");
|
||||
if (text.length > limit) {
|
||||
text = `${text.slice(0, limit)}\n> (近期工作記憶超過 ${limit} 字元預算已截斷;可用 ROLE_LOAD_INBOX_LIMIT 調整)`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function findMemory(role, memoryId) {
|
||||
for (const category of CATEGORIES) {
|
||||
const filePath = path.join(memoryRoot(role), category, `${memoryId}.md`);
|
||||
@@ -544,12 +569,16 @@ function cmdLoad(args) {
|
||||
blocks.push(`> 尚有 ${pending} 則未整理記憶,將於下次睡眠時段歸檔。`);
|
||||
}
|
||||
}
|
||||
if (!blocks.length) return 1;
|
||||
// 近期工作記憶用獨立預算,先算好;長期記憶維持原本的 ROLE_LOAD_LIMIT 額度不被擠壓
|
||||
const recentBlock = inboxBlock(args.role, args.inboxCount, args.inboxLimit);
|
||||
if (!blocks.length && !recentBlock) 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 (recentBlock) text = text ? `${recentBlock}\n\n${text}` : recentBlock;
|
||||
process.stdout.write(text);
|
||||
return 0;
|
||||
}
|
||||
@@ -944,6 +973,8 @@ function main(argv) {
|
||||
args.hours = Number.parseFloat(args.hours || "");
|
||||
args.idleMinutes = Number.parseFloat(args.idleMinutes || "");
|
||||
args.minInbox = Number.parseInt(args.minInbox || "", 10);
|
||||
args.inboxLimit = Number.parseInt(args.inboxLimit || "", 10);
|
||||
args.inboxCount = Number.parseInt(args.inboxCount || "", 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);
|
||||
@@ -953,6 +984,8 @@ function main(argv) {
|
||||
if (!Number.isFinite(args.hours)) args.hours = 20.0;
|
||||
if (!Number.isFinite(args.idleMinutes)) args.idleMinutes = 45.0;
|
||||
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);
|
||||
args.category ||= "important";
|
||||
args.tags ||= "";
|
||||
args.source ||= "";
|
||||
|
||||
Reference in New Issue
Block a user