feat(role): 新增心理學記憶型態 metadata

This commit is contained in:
Jeffery
2026-07-28 12:46:19 +08:00
parent 78f4086a48
commit 3a3114b8b3
3 changed files with 202 additions and 25 deletions
+167 -9
View File
@@ -4,7 +4,7 @@
// inbox(2) 產生 SessionStart 要注入的記憶區塊,(3) 睡眠整理時輸出待整理
// 素材並套用整理結果(NREM 鞏固/REM 整合、分類、去重、標籤、總結、
// 優先度、關聯、壓縮歸檔),(4) 依使用頻率與優先度遺忘日常與其他類記憶。
// 更新時間:2026/07/28 12:21:11
// 更新時間:2026/07/28 12:40:24
// 相依:Node.js 標準庫。
// 退出碼:0 成功;1 無內容可處理;2 參數錯誤。呼叫端(hook)一律不得因此中斷。
// ==============================================================================
@@ -29,6 +29,33 @@ const FULL_CATEGORIES = ["important", "interest"];
const DIGEST_CATEGORIES = ["skill", "news", "daily", "other"];
const FORGET_RULES = { daily: [14, 1], other: [7, 1] };
const DEFAULT_PRIORITY = { important: 5, interest: 4, skill: 4, news: 3, daily: 2, other: 1 };
const MEMORY_TYPES = ["semantic", "episodic", "procedural", "emotional", "preference", "rule"];
const MEMORY_TYPE_LABELS = {
semantic: "語意",
episodic: "情節",
procedural: "程序",
emotional: "情緒",
preference: "偏好",
rule: "規範",
};
const DEFAULT_MEMORY_TYPE = {
important: "preference",
interest: "emotional",
news: "semantic",
skill: "procedural",
daily: "episodic",
other: "semantic",
};
const MEMORY_TYPE_WEIGHT = {
rule: 50,
preference: 45,
procedural: 35,
semantic: 30,
emotional: 25,
episodic: 10,
};
const DECLARATIVE_VALUES = ["explicit", "implicit"];
const RETENTION_STAGES = ["working", "long_term"];
const SLEEP_BATCH = 60;
const COLLECT_LIMIT = 12000;
@@ -141,6 +168,75 @@ function normalizePriority(value, category = "other") {
return Math.max(1, Math.min(5, priority));
}
function normalizeMemoryType(value, category = "other") {
const raw = String(value || "").trim().toLowerCase().replace(/-/g, "_");
const alias = {
explicit: "semantic",
declarative: "semantic",
implicit: "procedural",
non_declarative: "procedural",
nondeclarative: "procedural",
semantic_memory: "semantic",
episodic_memory: "episodic",
procedural_memory: "procedural",
emotion: "emotional",
affective: "emotional",
pref: "preference",
preference_memory: "preference",
policy: "rule",
guideline: "rule",
規範: "rule",
偏好: "preference",
程序: "procedural",
技能: "procedural",
語意: "semantic",
知識: "semantic",
情節: "episodic",
事件: "episodic",
情緒: "emotional",
};
const normalized = alias[raw] || alias[String(value || "").trim()] || raw;
if (MEMORY_TYPES.includes(normalized)) return normalized;
return DEFAULT_MEMORY_TYPE[normalizeCategory(category)] || "semantic";
}
function normalizeDeclarative(value, memoryType = "semantic") {
const raw = String(value || "").trim().toLowerCase().replace(/-/g, "_");
const alias = {
declarative: "explicit",
explicit_memory: "explicit",
non_declarative: "implicit",
nondeclarative: "implicit",
implicit_memory: "implicit",
外顯: "explicit",
陳述性: "explicit",
內隱: "implicit",
非陳述性: "implicit",
};
const normalized = alias[raw] || alias[String(value || "").trim()] || raw;
if (DECLARATIVE_VALUES.includes(normalized)) return normalized;
return ["procedural", "emotional"].includes(memoryType) ? "implicit" : "explicit";
}
function normalizeRetentionStage(value, fallback = "long_term") {
const raw = String(value || "").trim().toLowerCase().replace(/-/g, "_");
const alias = {
short_term: "working",
working_memory: "working",
inbox: "working",
encoding: "working",
long: "long_term",
longterm: "long_term",
long_term_memory: "long_term",
短期: "working",
工作記憶: "working",
長期: "long_term",
};
const normalized = alias[raw] || alias[String(value || "").trim()] || raw;
if (RETENTION_STAGES.includes(normalized)) return normalized;
return fallback;
}
function oneLine(value, limit = 120) {
return String(value || "").replace(/\s+/g, " ").trim().slice(0, limit);
}
@@ -155,6 +251,9 @@ function dumpMemory(meta, content) {
"priority",
"relevance",
"links",
"memory_type",
"declarative",
"retention_stage",
"sleep_stage",
"created",
"updated",
@@ -204,6 +303,10 @@ function loadMemory(filePath) {
meta.relevance ||= [];
meta.links ||= [];
meta.priority = normalizePriority(meta.priority, meta.category || "other");
meta.memory_type = normalizeMemoryType(meta.memory_type, meta.category || "other");
meta.declarative = normalizeDeclarative(meta.declarative, meta.memory_type);
const retentionFallback = filePath.includes(`${path.sep}inbox${path.sep}`) || meta.sleep_stage === "encoding" ? "working" : "long_term";
meta.retention_stage = normalizeRetentionStage(meta.retention_stage, retentionFallback);
return [meta, body.trim()];
}
@@ -228,8 +331,18 @@ function listMemories(role, category) {
items.sort((a, b) => {
const am = a[0];
const bm = b[0];
const av = [normalizePriority(am.priority, category), am.links?.length ? 1 : 0, am.updated || ""];
const bv = [normalizePriority(bm.priority, category), bm.links?.length ? 1 : 0, bm.updated || ""];
const av = [
normalizePriority(am.priority, category),
MEMORY_TYPE_WEIGHT[am.memory_type] || 0,
am.links?.length ? 1 : 0,
am.updated || "",
];
const bv = [
normalizePriority(bm.priority, category),
MEMORY_TYPE_WEIGHT[bm.memory_type] || 0,
bm.links?.length ? 1 : 0,
bm.updated || "",
];
for (let i = 0; i < av.length; i += 1) {
if (av[i] < bv[i]) return 1;
if (av[i] > bv[i]) return -1;
@@ -267,7 +380,8 @@ function findMemory(role, memoryId) {
function memoryHint(meta) {
const relevance = (meta.relevance || []).join("、") || "-";
const links = (meta.links || []).join("、") || "-";
return `優先度:${normalizePriority(meta.priority, meta.category)};關聯:${relevance};連結:${links}`;
const type = MEMORY_TYPE_LABELS[meta.memory_type] || meta.memory_type || "語意";
return `優先度:${normalizePriority(meta.priority, meta.category)};型態:${type}/${meta.declarative || "explicit"};關聯:${relevance};連結:${links}`;
}
function archiveFile(filePath, destinationDir) {
@@ -283,7 +397,7 @@ function archiveFile(filePath, destinationDir) {
}
}
const FIELD_PATTERN = /^\s*(CATEGORY|SUMMARY|TAGS|PRIORITY|RELEVANCE|CONTENT)\s*[:]\s*(.*)$/i;
const FIELD_PATTERN = /^\s*(CATEGORY|SUMMARY|TAGS|PRIORITY|RELEVANCE|MEMORY_TYPE|DECLARATIVE|RETENTION_STAGE|CONTENT)\s*[:]\s*(.*)$/i;
function parseCapture(text) {
let category = "";
@@ -291,6 +405,9 @@ function parseCapture(text) {
let priority = null;
let tags = [];
let relevance = [];
let memoryType = "";
let declarative = "";
let retentionStage = "";
const contentLines = [];
let inContent = false;
for (const line of String(text || "").split(/\r?\n/)) {
@@ -303,6 +420,9 @@ function parseCapture(text) {
else if (field === "TAGS") tags = normalizeTags(value);
else if (field === "PRIORITY") priority = value;
else if (field === "RELEVANCE") relevance = normalizeList(value);
else if (field === "MEMORY_TYPE") memoryType = value;
else if (field === "DECLARATIVE") declarative = value;
else if (field === "RETENTION_STAGE") retentionStage = value;
else if (field === "CONTENT") {
inContent = true;
if (value.trim()) contentLines.push(value);
@@ -311,7 +431,17 @@ function parseCapture(text) {
}
if (inContent) contentLines.push(line);
}
return { category, summary, tags, priority, relevance, content: contentLines.join("\n").trim() };
return {
category,
summary,
tags,
priority,
relevance,
memoryType,
declarative,
retentionStage,
content: contentLines.join("\n").trim(),
};
}
function cmdWrite(args) {
@@ -319,6 +449,7 @@ function cmdWrite(args) {
if (!parsed.content && !parsed.summary) return 1;
const content = (parsed.content || parsed.summary).slice(0, CONTENT_LIMIT);
const stamp = nowStamp();
const memoryType = normalizeMemoryType(parsed.memoryType, parsed.category);
const meta = {
id: newId(content + stamp),
category: normalizeCategory(parsed.category),
@@ -327,6 +458,9 @@ function cmdWrite(args) {
priority: normalizePriority(parsed.priority, parsed.category),
relevance: parsed.relevance.length ? parsed.relevance : ["inbox"],
links: [],
memory_type: memoryType,
declarative: normalizeDeclarative(parsed.declarative, memoryType),
retention_stage: "working",
sleep_stage: "encoding",
created: stamp,
updated: stamp,
@@ -345,6 +479,7 @@ function cmdSeed(args) {
const content = (raw || args.summary).slice(0, CONTENT_LIMIT);
const stamp = nowStamp();
const category = normalizeCategory(args.category);
const memoryType = normalizeMemoryType(args.memoryType, category);
const meta = {
id: newId(content + args.summary + stamp),
category,
@@ -353,6 +488,9 @@ function cmdSeed(args) {
priority: normalizePriority(args.priority, category),
relevance: normalizeList(args.relevance).length ? normalizeList(args.relevance) : ["explicit", "background"],
links: [],
memory_type: memoryType,
declarative: normalizeDeclarative(args.declarative, memoryType),
retention_stage: "long_term",
sleep_stage: "seed",
created: stamp,
updated: stamp,
@@ -387,7 +525,8 @@ function cmdLoad(args) {
digestLines.push(`### ${CATEGORY_LABELS[category]}記憶(總結)`);
for (const [meta] of items) {
const priority = normalizePriority(meta.priority, category);
if (priority < args.digestMinPriority && !(meta.links || []).length) continue;
const durableType = ["rule", "preference", "procedural"].includes(meta.memory_type);
if (priority < args.digestMinPriority && !(meta.links || []).length && !durableType) continue;
const tags = (meta.tags || []).join("、") || "無標籤";
digestLines.push(`- ${meta.summary || "(無總結)"}(標籤:${tags}${memoryHint(meta)}`);
}
@@ -424,6 +563,7 @@ function cmdCollect(args) {
lines.push(`初判標籤: ${(meta.tags || []).join("、") || "無"}`);
lines.push(`初判優先度: ${normalizePriority(meta.priority, meta.category || "other")}`);
lines.push(`初判關聯: ${(meta.relevance || []).join("、") || "-"}`);
lines.push(`初判記憶型態: ${meta.memory_type} / ${meta.declarative} / ${meta.retention_stage}`);
lines.push("內容:", content, "");
}
@@ -432,7 +572,7 @@ function cmdCollect(args) {
for (const category of CATEGORIES) {
for (const [meta] of listMemories(args.role, category)) {
const tags = (meta.tags || []).join("、") || "無";
rows.push(`- id: ${meta.id} | 分類: ${CATEGORY_LABELS[category]} | 優先度: ${normalizePriority(meta.priority, category)} | 標籤: ${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) {
@@ -499,6 +639,9 @@ function cmdApply(args) {
const priority = normalizePriority(entry.priority, entryCategory);
const relevance = normalizeList(entry.relevance);
const links = normalizeList(entry.links);
const memoryType = normalizeMemoryType(entry.memory_type || entry.memoryType, entryCategory);
const declarative = normalizeDeclarative(entry.declarative, memoryType);
const retentionStage = normalizeRetentionStage(entry.retention_stage || entry.retentionStage, "long_term");
const sleepStage = oneLine(entry.sleep_stage || entry.sleepStage || "nrem-rem", 40);
if (!content && !summary) continue;
@@ -509,6 +652,7 @@ function cmdApply(args) {
action = "new";
} else {
const category = normalizeCategory(entry.category || meta.category);
const mergedMemoryType = normalizeMemoryType(entry.memory_type || entry.memoryType || meta.memory_type, category);
const newMeta = {
id: meta.id,
category,
@@ -517,6 +661,9 @@ function cmdApply(args) {
priority: Math.max(normalizePriority(meta.priority, category), priority),
relevance: normalizeList([...(meta.relevance || []), ...relevance]),
links: normalizeList([...(meta.links || []), ...links]),
memory_type: mergedMemoryType,
declarative: normalizeDeclarative(entry.declarative || meta.declarative, mergedMemoryType),
retention_stage: normalizeRetentionStage(entry.retention_stage || entry.retentionStage || meta.retention_stage, "long_term"),
sleep_stage: sleepStage,
created: meta.created || stamp,
updated: stamp,
@@ -546,6 +693,9 @@ function cmdApply(args) {
priority,
relevance,
links,
memory_type: memoryType,
declarative,
retention_stage: retentionStage,
sleep_stage: sleepStage,
created: stamp,
updated: stamp,
@@ -581,10 +731,13 @@ function cmdForget(args) {
for (const [meta] of listMemories(args.role, category)) {
const updated = parseStamp(meta.updated) || parseStamp(meta.created);
if (!updated) continue;
if ((now - updated) / 86400000 < days) continue;
const ageDays = (now - updated) / 86400000;
const effectiveDays = meta.memory_type === "episodic" ? Math.max(3, Math.ceil(days / 2)) : days;
if (ageDays < effectiveDays) continue;
if (Number.parseInt(meta.hits || 0, 10) > maxHits) continue;
if (normalizePriority(meta.priority, category) > 2) continue;
if ((meta.links || []).length) continue;
if (["rule", "preference", "procedural"].includes(meta.memory_type)) continue;
if (args.dryRun) {
forgotten.push(`${CATEGORY_LABELS[category]}${meta.summary || ""}`);
continue;
@@ -608,8 +761,10 @@ function cmdStats(args) {
ensureLayout(args.role);
const state = readState(args.role);
const rows = ["| 分類 | 筆數 | 平均優先度 |", "| --- | --- | --- |"];
const typeCounts = Object.fromEntries(MEMORY_TYPES.map((type) => [type, 0]));
for (const category of CATEGORIES) {
const items = listMemories(args.role, category);
for (const [meta] of items) typeCounts[meta.memory_type] = (typeCounts[meta.memory_type] || 0) + 1;
let avg = "-";
if (items.length) {
const value = items.reduce((sum, [meta]) => sum + normalizePriority(meta.priority, category), 0) / items.length;
@@ -623,6 +778,7 @@ function cmdStats(args) {
rows.push(`- 上次睡眠整理:${state.last_sleep || "尚未整理"}`);
rows.push(`- 上次睡眠摘要:${state.last_sleep_digest || "尚無"}`);
rows.push(`- 上次遺忘:${state.last_forget || "尚未執行"}`);
rows.push(`- 記憶型態:${MEMORY_TYPES.map((type) => `${MEMORY_TYPE_LABELS[type]} ${typeCounts[type] || 0}`).join("、")}`);
process.stdout.write(rows.join("\n"));
return 0;
}
@@ -703,6 +859,8 @@ function main(argv) {
args.source ||= "";
args.priority ||= "4";
args.relevance ||= "explicit,background";
args.memoryType ||= "";
args.declarative ||= "";
args.summary ||= "";
args.project ||= "";