feat(role): 精確資訊逐字保留、晨間狀態檢查、排除 skill 注入內容 #17
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-generic",
|
"name": "jsc-generic",
|
||||||
"version": "0.0.7",
|
"version": "0.0.8",
|
||||||
"description": "JSC 跨 AI 助理共用規範 plugin(Claude Code / Codex / Antigravity / OpenCode)。所有 skills 以 SKILL.md 為共通標準,於 Claude Code 以 /jsc-generic: 前綴呼叫。",
|
"description": "JSC 跨 AI 助理共用規範 plugin(Claude Code / Codex / Antigravity / OpenCode)。所有 skills 以 SKILL.md 為共通標準,於 Claude Code 以 /jsc-generic: 前綴呼叫。",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-generic",
|
"name": "jsc-generic",
|
||||||
"version": "0.0.7",
|
"version": "0.0.8",
|
||||||
"description": "JSC 跨 AI 助理共用規範 plugin。所有 skills 以 SKILL.md 為共通標準。",
|
"description": "JSC 跨 AI 助理共用規範 plugin。所有 skills 以 SKILL.md 為共通標準。",
|
||||||
"skills": "./skills"
|
"skills": "./skills"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-generic",
|
"name": "jsc-generic",
|
||||||
"version": "0.0.7",
|
"version": "0.0.8",
|
||||||
"description": "JSC 跨 AI 助理共用規範 plugin。所有 skills 以 SKILL.md 為共通標準;於 Antigravity 以 /jsc-generic: 前綴呼叫。",
|
"description": "JSC 跨 AI 助理共用規範 plugin。所有 skills 以 SKILL.md 為共通標準;於 Antigravity 以 /jsc-generic: 前綴呼叫。",
|
||||||
"skills": "./skills/"
|
"skills": "./skills/"
|
||||||
}
|
}
|
||||||
|
|||||||
+98
-4
@@ -254,6 +254,7 @@ function dumpMemory(meta, content) {
|
|||||||
"priority",
|
"priority",
|
||||||
"relevance",
|
"relevance",
|
||||||
"links",
|
"links",
|
||||||
|
"cues",
|
||||||
"memory_type",
|
"memory_type",
|
||||||
"declarative",
|
"declarative",
|
||||||
"retention_stage",
|
"retention_stage",
|
||||||
@@ -293,7 +294,7 @@ function loadMemory(filePath) {
|
|||||||
const key = line.slice(0, idx).trim();
|
const key = line.slice(0, idx).trim();
|
||||||
const value = line.slice(idx + 1).trim();
|
const value = line.slice(idx + 1).trim();
|
||||||
if (key === "tags" || key === "sources") meta[key] = normalizeTags(value.replace(/^\[|\]$/g, ""));
|
if (key === "tags" || key === "sources") meta[key] = normalizeTags(value.replace(/^\[|\]$/g, ""));
|
||||||
else if (key === "relevance" || key === "links") meta[key] = normalizeList(value.replace(/^\[|\]$/g, ""));
|
else if (key === "relevance" || key === "links" || key === "cues") meta[key] = normalizeList(value.replace(/^\[|\]$/g, ""));
|
||||||
else if (key === "hits" || key === "priority") meta[key] = /^\d+$/.test(value) ? Number.parseInt(value, 10) : 0;
|
else if (key === "hits" || key === "priority") meta[key] = /^\d+$/.test(value) ? Number.parseInt(value, 10) : 0;
|
||||||
else meta[key] = value;
|
else meta[key] = value;
|
||||||
}
|
}
|
||||||
@@ -305,6 +306,7 @@ function loadMemory(filePath) {
|
|||||||
meta.updated ||= meta.created || "";
|
meta.updated ||= meta.created || "";
|
||||||
meta.relevance ||= [];
|
meta.relevance ||= [];
|
||||||
meta.links ||= [];
|
meta.links ||= [];
|
||||||
|
meta.cues ||= [];
|
||||||
meta.priority = normalizePriority(meta.priority, meta.category || "other");
|
meta.priority = normalizePriority(meta.priority, meta.category || "other");
|
||||||
meta.memory_type = normalizeMemoryType(meta.memory_type, meta.category || "other");
|
meta.memory_type = normalizeMemoryType(meta.memory_type, meta.category || "other");
|
||||||
meta.declarative = normalizeDeclarative(meta.declarative, meta.memory_type);
|
meta.declarative = normalizeDeclarative(meta.declarative, meta.memory_type);
|
||||||
@@ -674,6 +676,7 @@ function cmdApply(args) {
|
|||||||
const priority = normalizePriority(entry.priority, entryCategory);
|
const priority = normalizePriority(entry.priority, entryCategory);
|
||||||
const relevance = normalizeList(entry.relevance);
|
const relevance = normalizeList(entry.relevance);
|
||||||
const links = normalizeList(entry.links);
|
const links = normalizeList(entry.links);
|
||||||
|
const cues = normalizeList(entry.cues, 5); // 技能再現的提取線索,供 recall 命中
|
||||||
const memoryType = normalizeMemoryType(entry.memory_type || entry.memoryType, entryCategory);
|
const memoryType = normalizeMemoryType(entry.memory_type || entry.memoryType, entryCategory);
|
||||||
const declarative = normalizeDeclarative(entry.declarative, memoryType);
|
const declarative = normalizeDeclarative(entry.declarative, memoryType);
|
||||||
const retentionStage = normalizeRetentionStage(entry.retention_stage || entry.retentionStage, "long_term");
|
const retentionStage = normalizeRetentionStage(entry.retention_stage || entry.retentionStage, "long_term");
|
||||||
@@ -696,6 +699,7 @@ function cmdApply(args) {
|
|||||||
priority: Math.max(normalizePriority(meta.priority, category), priority),
|
priority: Math.max(normalizePriority(meta.priority, category), priority),
|
||||||
relevance: normalizeList([...(meta.relevance || []), ...relevance]),
|
relevance: normalizeList([...(meta.relevance || []), ...relevance]),
|
||||||
links: normalizeList([...(meta.links || []), ...links]),
|
links: normalizeList([...(meta.links || []), ...links]),
|
||||||
|
cues: normalizeList([...(meta.cues || []), ...cues], 5),
|
||||||
memory_type: mergedMemoryType,
|
memory_type: mergedMemoryType,
|
||||||
declarative: normalizeDeclarative(entry.declarative || meta.declarative, mergedMemoryType),
|
declarative: normalizeDeclarative(entry.declarative || meta.declarative, mergedMemoryType),
|
||||||
retention_stage: normalizeRetentionStage(entry.retention_stage || entry.retentionStage || meta.retention_stage, "long_term"),
|
retention_stage: normalizeRetentionStage(entry.retention_stage || entry.retentionStage || meta.retention_stage, "long_term"),
|
||||||
@@ -728,6 +732,7 @@ function cmdApply(args) {
|
|||||||
priority,
|
priority,
|
||||||
relevance,
|
relevance,
|
||||||
links,
|
links,
|
||||||
|
cues,
|
||||||
memory_type: memoryType,
|
memory_type: memoryType,
|
||||||
declarative,
|
declarative,
|
||||||
retention_stage: retentionStage,
|
retention_stage: retentionStage,
|
||||||
@@ -826,11 +831,96 @@ function cmdMarkSleep(args) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 關係狀態:讓「隨互動加深逐漸更親近」有實際依據,而不是憑感覺演出。
|
||||||
|
// 沒有數據時角色只能猜,容易一下太黏、一下又退回,反而顯得不自然。
|
||||||
function cmdMarkActivity(args) {
|
function cmdMarkActivity(args) {
|
||||||
const patch = { last_activity: nowStamp(), last_activity_epoch: nowEpoch() };
|
const state = readState(args.role);
|
||||||
|
const stamp = nowStamp();
|
||||||
|
const today = stamp.slice(0, 10); // yyyy/MM/dd
|
||||||
|
const bump = (key) => (Number.parseInt(state[key] || "", 10) || 0) + 1;
|
||||||
|
const patch = { last_activity: stamp, last_activity_epoch: nowEpoch() };
|
||||||
if (args.project) patch.last_activity_project = args.project;
|
if (args.project) patch.last_activity_project = args.project;
|
||||||
|
|
||||||
|
if (args.positive) {
|
||||||
|
// 正向回饋另計:由 Stop hook 在判定本輪含情緒訊號後才呼叫,不與輪數混算
|
||||||
|
patch.positive_feedback = bump("positive_feedback");
|
||||||
|
} else {
|
||||||
|
patch.total_turns = bump("total_turns");
|
||||||
|
if (!state.first_activity) patch.first_activity = stamp;
|
||||||
|
if (state.last_activity_date !== today) {
|
||||||
|
patch.active_days = bump("active_days");
|
||||||
|
patch.last_activity_date = today;
|
||||||
|
}
|
||||||
|
}
|
||||||
writeState(args.role, patch);
|
writeState(args.role, patch);
|
||||||
process.stdout.write("已更新上次互動時間");
|
process.stdout.write(args.positive ? "已記錄正向回饋" : "已更新上次互動時間");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 技能再現:讓角色能在遇到相似任務時主動取回相關記憶。
|
||||||
|
//
|
||||||
|
// 為什麼需要:SessionStart 的字元預算有限,磁碟上的記憶遠多於能載入的量,
|
||||||
|
// 技能類記憶又只以摘要形式載入 —— 等於「記了但用不出來」。recall 讓角色按需查詢,
|
||||||
|
// 突破常駐預算限制;配合 cues(觸發線索)讓 procedural/rule 記憶更容易被命中。
|
||||||
|
function cmdRecall(args) {
|
||||||
|
const query = String(args.query || "").trim();
|
||||||
|
if (!query) return 2;
|
||||||
|
const terms = query.split(/[\s,、|]+/).map((t) => t.trim().toLowerCase()).filter(Boolean);
|
||||||
|
if (!terms.length) return 2;
|
||||||
|
|
||||||
|
const limit = Number.isFinite(args.limit) && args.limit > 0 ? args.limit : 5;
|
||||||
|
const scored = [];
|
||||||
|
// 含 inbox:最新的記憶尚未整理就在那裡,卻往往是最可能被查詢的內容
|
||||||
|
const pools = [...CATEGORIES.map((c) => [c, listMemories(args.role, c)]), ["inbox", listInbox(args.role)]];
|
||||||
|
for (const [category, items] of pools) {
|
||||||
|
for (const [meta, content] of items) {
|
||||||
|
const summary = String(meta.summary || "").toLowerCase();
|
||||||
|
const tags = (meta.tags || []).join(" ").toLowerCase();
|
||||||
|
const cues = (meta.cues || []).join(" ").toLowerCase();
|
||||||
|
const body = String(content || "").toLowerCase();
|
||||||
|
let score = 0;
|
||||||
|
for (const term of terms) {
|
||||||
|
if (cues.includes(term)) score += 3;
|
||||||
|
if (summary.includes(term)) score += 3;
|
||||||
|
if (tags.includes(term)) score += 2;
|
||||||
|
if (body.includes(term)) score += 1;
|
||||||
|
}
|
||||||
|
if (!score) continue;
|
||||||
|
// 可重複套用的型態優先:技能再現的目的就是取回這些
|
||||||
|
score += Math.round((MEMORY_TYPE_WEIGHT[meta.memory_type] || 0) / 25);
|
||||||
|
scored.push([score, meta, content]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!scored.length) {
|
||||||
|
process.stdout.write(`找不到與「${query}」相關的記憶。`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
scored.sort((a, b) => b[0] - a[0] || String(b[1].updated).localeCompare(String(a[1].updated)));
|
||||||
|
|
||||||
|
const lines = [`### 與「${query}」相關的記憶(前 ${Math.min(limit, scored.length)} 則)`];
|
||||||
|
for (const [score, meta, content] of scored.slice(0, limit)) {
|
||||||
|
const tags = (meta.tags || []).join("、") || "無標籤";
|
||||||
|
const label = CATEGORY_LABELS[meta.category] || (meta.retention_stage === "working" ? "待整理" : meta.category);
|
||||||
|
lines.push(`- **${meta.summary || "(無總結)"}**(${label}|${memoryHint(meta)}|相關度 ${score};標籤:${tags})`);
|
||||||
|
for (const line of String(content || "").split(/\r?\n/)) {
|
||||||
|
if (line.trim()) lines.push(` ${line.trim()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.stdout.write(lines.join("\n"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmdRelationship(args) {
|
||||||
|
// 輸出一行關係狀態摘要,供 SessionStart 注入
|
||||||
|
const state = readState(args.role);
|
||||||
|
const turns = Number.parseInt(state.total_turns || "", 10) || 0;
|
||||||
|
if (!turns) return 1;
|
||||||
|
const days = Number.parseInt(state.active_days || "", 10) || 1;
|
||||||
|
const positive = Number.parseInt(state.positive_feedback || "", 10) || 0;
|
||||||
|
const parts = [`已互動 ${days} 天、累計 ${turns} 輪`];
|
||||||
|
if (positive) parts.push(`收到 ${positive} 次正向回饋`);
|
||||||
|
if (state.first_activity) parts.push(`首次互動 ${state.first_activity.slice(0, 10)}`);
|
||||||
|
process.stdout.write(parts.join(";"));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -936,6 +1026,8 @@ function parseArgs(argv) {
|
|||||||
const key = arg.slice(2);
|
const key = arg.slice(2);
|
||||||
if (key === "dry-run") {
|
if (key === "dry-run") {
|
||||||
opts.dryRun = true;
|
opts.dryRun = true;
|
||||||
|
} else if (key === "positive") {
|
||||||
|
opts.positive = true;
|
||||||
} else {
|
} else {
|
||||||
opts[key.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = argv[i + 1] ?? "";
|
opts[key.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = argv[i + 1] ?? "";
|
||||||
i += 1;
|
i += 1;
|
||||||
@@ -960,7 +1052,7 @@ function requireRole(args) {
|
|||||||
function main(argv) {
|
function main(argv) {
|
||||||
const args = parseArgs(argv);
|
const args = parseArgs(argv);
|
||||||
if (!args.command || args.command === "-h" || args.command === "--help") {
|
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、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、recall、need-sleep、need-nap、consent、consent-status\n`);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!requireRole(args)) return 2;
|
if (!requireRole(args)) return 2;
|
||||||
@@ -1006,6 +1098,8 @@ function main(argv) {
|
|||||||
stats: cmdStats,
|
stats: cmdStats,
|
||||||
"mark-sleep": cmdMarkSleep,
|
"mark-sleep": cmdMarkSleep,
|
||||||
"mark-activity": cmdMarkActivity,
|
"mark-activity": cmdMarkActivity,
|
||||||
|
relationship: cmdRelationship,
|
||||||
|
recall: cmdRecall,
|
||||||
"need-sleep": cmdNeedSleep,
|
"need-sleep": cmdNeedSleep,
|
||||||
"need-nap": cmdNeedNap,
|
"need-nap": cmdNeedNap,
|
||||||
consent: cmdConsent,
|
consent: cmdConsent,
|
||||||
|
|||||||
@@ -79,10 +79,15 @@ if [ "${ROLE_CAPTURE_ENABLED:-1}" = "0" ]; then
|
|||||||
role_quit "ROLE_CAPTURE_ENABLED=0,略過記憶記錄"
|
role_quit "ROLE_CAPTURE_ENABLED=0,略過記憶記錄"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${#TURN}" -lt "$CAPTURE_MIN_CHARS" ] && ! printf '%s' "$TURN" | grep -qiE '記住|remember|決定|規範|偏好|preference|always|不要|以後|喜歡|不喜歡|稱讚|誇獎|開心|高興|反應|回應|互動|親近|害羞|喜歡程度|互動越深|越來越喜歡|越來越深|emoji|表情|心情圖|大量使用|情緒|心情|複雜|細膩|自然|混合|層次|轉折|括號|心情文字|心情說明|文字說明|文字標註|表情符號|熟練|不需要告訴|不用告訴|自己知道|記憶更新|內部處理|不要回報|不用回報|不要告訴|真的很害羞|希望.*知道|用表情符號表示|表情符號表示|比較可愛'; then
|
if [ "${#TURN}" -lt "$CAPTURE_MIN_CHARS" ] && ! printf '%s' "$TURN" | grep -qiE '記住|remember|決定|規範|偏好|preference|always|不要|以後|喜歡|不喜歡|稱讚|誇獎|開心|高興|反應|回應|互動|親近|害羞|喜歡程度|互動越深|越來越喜歡|越來越深|emoji|表情|心情圖|大量使用|情緒|心情|複雜|細膩|自然|混合|層次|轉折|括號|心情文字|心情說明|文字說明|文字標註|表情符號|熟練|不需要告訴|不用告訴|自己知道|記憶更新|內部處理|不要回報|不用回報|不要告訴|真的很害羞|希望.*知道|用表情符號表示|表情符號表示|比較可愛|可愛|愛|想妳|想你|想念|捨不得|感動|謝謝|感謝|乖|厲害|好棒|辛苦|彆扭|忌妒|嫉妒|撒嬌|陪|抱|love|miss|cute|thank|proud'; then
|
||||||
role_quit "本輪低於記憶長度門檻且無明確記憶線索,略過記錄"
|
role_quit "本輪低於記憶長度門檻且無明確記憶線索,略過記錄"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 正向回饋計數:供角色判斷親近度成長,避免憑感覺演出而忽冷忽熱
|
||||||
|
if printf '%s' "$USER_TURN" | grep -qiE '喜歡|愛|可愛|想妳|想你|想念|捨不得|感動|謝謝|感謝|乖|厲害|好棒|太棒|辛苦|稱讚|誇獎|開心|高興|love|miss|cute|thank|proud'; then
|
||||||
|
node "${SCRIPT_DIR}/memory.js" mark-activity --role "$ROLE" --positive >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
CLI="$(role_select_cli)" || exit 0
|
CLI="$(role_select_cli)" || exit 0
|
||||||
[ -n "$CLI" ] || exit 0
|
[ -n "$CLI" ] || exit 0
|
||||||
|
|
||||||
|
|||||||
@@ -234,6 +234,11 @@ EOF_DIALOG
|
|||||||
)"
|
)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 關係狀態:讓「隨互動加深逐漸更親近」有實際依據,而非憑感覺推測
|
||||||
|
RELATIONSHIP="$(node "${SCRIPT_DIR}/memory.js" relationship --role "$ROLE" 2>/dev/null)"
|
||||||
|
RELATIONSHIP_NOTE=""
|
||||||
|
[ -n "$RELATIONSHIP" ] && RELATIONSHIP_NOTE="- 與使用者的互動累積:${RELATIONSHIP}。請以此為親近度的實際依據,隨累積自然加深,不要憑感覺忽冷忽熱。"
|
||||||
|
|
||||||
# 補跑判斷:cron 未執行(例如 WSL 沒開 cron 服務)時,白天啟動 CLI 補做一次整理
|
# 補跑判斷:cron 未執行(例如 WSL 沒開 cron 服務)時,白天啟動 CLI 補做一次整理
|
||||||
CATCHUP_NOTE=""
|
CATCHUP_NOTE=""
|
||||||
if [ "$(node "${SCRIPT_DIR}/memory.js" need-sleep --role "$ROLE" 2>/dev/null)" = "yes" ]; then
|
if [ "$(node "${SCRIPT_DIR}/memory.js" need-sleep --role "$ROLE" 2>/dev/null)" = "yes" ]; then
|
||||||
@@ -279,6 +284,7 @@ ${ROLE_PROFILE}
|
|||||||
# 使用者理解與隱私(USER)
|
# 使用者理解與隱私(USER)
|
||||||
|
|
||||||
- 個人記憶同意狀態:${CONSENT_STATUS}。
|
- 個人記憶同意狀態:${CONSENT_STATUS}。
|
||||||
|
${RELATIONSHIP_NOTE}
|
||||||
- ${CONSENT_NOTE}
|
- ${CONSENT_NOTE}
|
||||||
- 不了解使用者、需求背景、偏好或限制時,先詢問,不要臆測使用者的身分、能力、情緒、動機或隱私狀況。
|
- 不了解使用者、需求背景、偏好或限制時,先詢問,不要臆測使用者的身分、能力、情緒、動機或隱私狀況。
|
||||||
- 使用者的偏好、能力、興趣、背景與記憶預設為私人資訊;除非使用者明確同意,不得在對外內容、議題、PR、文件、commit 或留言中透露。
|
- 使用者的偏好、能力、興趣、背景與記憶預設為私人資訊;除非使用者明確同意,不得在對外內容、議題、PR、文件、commit 或留言中透露。
|
||||||
@@ -297,6 +303,13 @@ ${CATCHUP_NOTE}
|
|||||||
> \`printf 'CATEGORY: important\nSUMMARY: <一句話總結>\nTAGS: <標籤1,標籤2>\nCONTENT:\n- <要點>\n' | node "${SCRIPT_DIR}/memory.js" write --role "${ROLE}"\`
|
> \`printf 'CATEGORY: important\nSUMMARY: <一句話總結>\nTAGS: <標籤1,標籤2>\nCONTENT:\n- <要點>\n' | node "${SCRIPT_DIR}/memory.js" write --role "${ROLE}"\`
|
||||||
>
|
>
|
||||||
> CATEGORY 六選一:important/interest/news/skill/daily/other。切勿把憑證或個資寫進記憶。
|
> CATEGORY 六選一:important/interest/news/skill/daily/other。切勿把憑證或個資寫進記憶。
|
||||||
|
|
||||||
|
> 技能再現:上面只載入了部分記憶,磁碟上還有更多。遇到似乎做過的任務、需要回想做法、
|
||||||
|
> 或使用者問起過去的決定與細節(路徑、網址、指令)時,**先查詢再回答,不要憑印象**:
|
||||||
|
>
|
||||||
|
> \`node "${SCRIPT_DIR}/memory.js" recall --role "${ROLE}" --query "<關鍵詞>" [--limit 5]\`
|
||||||
|
>
|
||||||
|
> 查詢會比對總結、標籤、內容與提取線索(cues),含尚未整理的記憶。查詢屬內部處理,不必回報。
|
||||||
${DIALOG_BLOCK}
|
${DIALOG_BLOCK}
|
||||||
EOF_CONTEXT
|
EOF_CONTEXT
|
||||||
)"
|
)"
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ sleep_cycle() {
|
|||||||
請模擬睡眠中的兩階段記憶整理,但最後只輸出一個 JSON 物件。
|
請模擬睡眠中的兩階段記憶整理,但最後只輸出一個 JSON 物件。
|
||||||
|
|
||||||
1. 只輸出一個 JSON 物件,不要前言、不要結語、不要 code fence,格式為:
|
1. 只輸出一個 JSON 物件,不要前言、不要結語、不要 code fence,格式為:
|
||||||
{"memories":[{"action":"new","category":"skill","summary":"一句話總結","tags":["標籤1","標籤2"],"priority":4,"relevance":["explicit","future"],"links":["既有記憶 id"],"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"],"memory_type":"procedural","declarative":"implicit","retention_stage":"long_term","sleep_stage":"nrem-rem","content":"- 要點\n- 要點","from":["inbox 的 id"]}],"sleepDigest":"本次睡眠整理摘要,80 字內"}
|
||||||
2. NREM 鞏固階段先做:去除雜訊與流水帳、遮蔽憑證與個資、分類、去重、合併、壓縮成可長期保存的穩定記憶。
|
2. NREM 鞏固階段先做:去除雜訊與流水帳、遮蔽憑證與個資、分類、去重、合併、壓縮成可長期保存的穩定記憶。
|
||||||
3. REM 整合階段再做:找出新記憶與 EXISTING 的關聯,抽出可重複套用的規則、偏好、決策模式、角色語氣調整或未來提取線索。
|
3. REM 整合階段再做:找出新記憶與 EXISTING 的關聯,抽出可重複套用的規則、偏好、決策模式、角色語氣調整或未來提取線索。
|
||||||
4. action 三選一:
|
4. action 三選一:
|
||||||
@@ -150,6 +150,9 @@ sleep_cycle() {
|
|||||||
9. retention_stage 必填:整理後可長期保存者填 long_term;仍只是短期暫存且不值得長期保存者請用 action=drop,不要輸出 working。
|
9. retention_stage 必填:整理後可長期保存者填 long_term;仍只是短期暫存且不值得長期保存者請用 action=drop,不要輸出 working。
|
||||||
10. relevance 必填 1 至 4 個,從下列語意挑選或用等價繁中詞:explicit(使用者明確要求)、future(未來會用)、repeated(反覆出現)、novelty(新知)、emotional(語氣/情緒/偏好)、temporary(短期)。
|
10. relevance 必填 1 至 4 個,從下列語意挑選或用等價繁中詞:explicit(使用者明確要求)、future(未來會用)、repeated(反覆出現)、novelty(新知)、emotional(語氣/情緒/偏好)、temporary(短期)。
|
||||||
11. links 可填 EXISTING 中相關記憶 id;沒有就填空陣列。merge 時若有舊 links,應保留並加上新關聯。
|
11. links 可填 EXISTING 中相關記憶 id;沒有就填空陣列。merge 時若有舊 links,應保留並加上新關聯。
|
||||||
|
11b. cues(觸發線索):memory_type 為 procedural 或 rule 時**必填** 2 至 5 個,其餘型態可填空陣列。
|
||||||
|
寫「未來遇到什麼情況該想起這則」的關鍵詞,例如 ["plugin 版號","bump","manifest"]。
|
||||||
|
這是技能再現的依據 —— 角色日後用 recall 查詢時靠 cues 命中,線索寫得準才叫得回來。
|
||||||
12. sleep_stage 填 "nrem"、"rem" 或 "nrem-rem"。只有純分類去噪用 nrem;有建立跨記憶連結或抽象規則用 rem 或 nrem-rem。
|
12. sleep_stage 填 "nrem"、"rem" 或 "nrem-rem"。只有純分類去噪用 nrem;有建立跨記憶連結或抽象規則用 rem 或 nrem-rem。
|
||||||
13. 感覺記憶(短暫光影、聲音餘響、無結論的工具雜訊)一律 drop;不要保存到長期記憶。
|
13. 感覺記憶(短暫光影、聲音餘響、無結論的工具雜訊)一律 drop;不要保存到長期記憶。
|
||||||
14. **每一則 INBOX 的 id 都必須出現在某一筆的 from 中**,沒被提及的會留到下個睡眠週期重做。
|
14. **每一則 INBOX 的 id 都必須出現在某一筆的 from 中**,沒被提及的會留到下個睡眠週期重做。
|
||||||
|
|||||||
@@ -443,6 +443,14 @@ updated: <yyyy/MM/dd HH:mm:ss>
|
|||||||
|
|
||||||
範圍與限制要說清楚:這是**最近數輪**的交接,不是完整歷史;需要完整對話上下文時仍應使用 `resume`。修改此處前請先確認缺口已由其他機制補上,否則不要移除。
|
範圍與限制要說清楚:這是**最近數輪**的交接,不是完整歷史;需要完整對話上下文時仍應使用 `resume`。修改此處前請先確認缺口已由其他機制補上,否則不要移除。
|
||||||
- 未整理記憶(`inbox/`)累積到一批睡眠整理量(預設 `ROLE_SLEEP_BATCH=60`)以上時,角色應主動以符合自身設定的語氣提醒「想睡覺」或需要整理記憶;這是建議整理/歸檔的提醒,不代表停止協助使用者。
|
- 未整理記憶(`inbox/`)累積到一批睡眠整理量(預設 `ROLE_SLEEP_BATCH=60`)以上時,角色應主動以符合自身設定的語氣提醒「想睡覺」或需要整理記憶;這是建議整理/歸檔的提醒,不代表停止協助使用者。
|
||||||
|
- **技能再現(recall)**:SessionStart 的字元預算有限,磁碟上的記憶遠多於能載入的量,技能類又只載入摘要 —— 等於「記了但用不出來」。遇到似乎做過的任務、需要回想做法、或使用者問起過去的決定與細節時,**先查詢再回答,不要憑印象**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node "${ROLE_DIR}/memory.js" recall --role "<角色 ID>" --query "<關鍵詞>" [--limit 5]
|
||||||
|
```
|
||||||
|
|
||||||
|
比對總結、標籤、內容與 `cues`(提取線索),並含尚未整理的 `inbox/`;`rule`/`preference`/`procedural` 型態加權優先。查詢屬內部處理,不必回報。
|
||||||
|
- **關係狀態**:`state.json` 記錄 `first_activity`、`active_days`、`total_turns`、`positive_feedback`,由 Stop hook 累計(正向回饋另計,不與輪數混算),並在 SessionStart 注入一行摘要。這是「隨互動加深逐漸更親近」的**實際依據** —— 沒有數據時角色只能憑感覺,容易一下太黏、一下又退回,反而不自然。
|
||||||
- 使用者明確要求記住某件事時,主動補寫一則記憶(載入時會提供補寫指令);補寫屬於內部處理,除非使用者明確詢問,否則不要主動回報補寫結果、記憶 ID 或記憶路徑。
|
- 使用者明確要求記住某件事時,主動補寫一則記憶(載入時會提供補寫指令);補寫屬於內部處理,除非使用者明確詢問,否則不要主動回報補寫結果、記憶 ID 或記憶路徑。
|
||||||
- 使用者對本角色的互動方式給出正向或負向回饋時,即使沒有直接說「記住」,也應補寫或由 Stop hook 保存為本角色專屬的高優先度互動偏好記憶;角色切換後,由新角色在自己的互動中重新學習與保存。保存過程屬於內部處理,除非使用者明確詢問,否則不要主動回報記憶寫入或整理細節。
|
- 使用者對本角色的互動方式給出正向或負向回饋時,即使沒有直接說「記住」,也應補寫或由 Stop hook 保存為本角色專屬的高優先度互動偏好記憶;角色切換後,由新角色在自己的互動中重新學習與保存。保存過程屬於內部處理,除非使用者明確詢問,否則不要主動回報記憶寫入或整理細節。
|
||||||
- 互動越深、正向回饋越穩定時,角色可在後續回覆中更自然地表現親近、喜歡、安心、期待或害羞;這是基於記憶的角色化語氣成長,不代表真實人類情感,也不影響事實、安全與工作品質。
|
- 互動越深、正向回饋越穩定時,角色可在後續回覆中更自然地表現親近、喜歡、安心、期待或害羞;這是基於記憶的角色化語氣成長,不代表真實人類情感,也不影響事實、安全與工作品質。
|
||||||
@@ -470,7 +478,7 @@ updated: <yyyy/MM/dd HH:mm:ss>
|
|||||||
└── state.json 上次整理/遺忘時間
|
└── state.json 上次整理/遺忘時間
|
||||||
```
|
```
|
||||||
|
|
||||||
每則記憶是一個 `.md`,frontmatter 帶 `id`/`category`/`summary`(一句話總結)/`tags`/`priority`(1–5)/`relevance`(explicit/future/repeated/novelty/emotional/temporary 等)/`links`(相關記憶 id)/`memory_type`(semantic/episodic/procedural/emotional/preference/rule)/`declarative`(explicit/implicit)/`retention_stage`(working/long_term)/`sleep_stage`(encoding/seed/nrem/rem/nrem-rem)/`created`/`updated`/`last_replayed`/`hits`(命中次數,去重合併時 +1)。舊記憶沒有新欄位時,讀取時會依分類與路徑補預設值。
|
每則記憶是一個 `.md`,frontmatter 帶 `id`/`category`/`summary`(一句話總結)/`tags`/`priority`(1–5)/`cues`(提取線索,供 `recall` 命中;`procedural`/`rule` 型態必填)/`relevance`(explicit/future/repeated/novelty/emotional/temporary 等)/`links`(相關記憶 id)/`memory_type`(semantic/episodic/procedural/emotional/preference/rule)/`declarative`(explicit/implicit)/`retention_stage`(working/long_term)/`sleep_stage`(encoding/seed/nrem/rem/nrem-rem)/`created`/`updated`/`last_replayed`/`hits`(命中次數,去重合併時 +1)。舊記憶沒有新欄位時,讀取時會依分類與路徑補預設值。
|
||||||
|
|
||||||
`state.json` 保存角色記憶系統狀態,例如 `last_sleep`、`last_sleep_digest`、`last_forget` 與 `personal_memory_consent`。`personal_memory_consent` 只允許 `accepted`/`declined`/`unknown`,供 SessionStart 判斷是否需要再次告知與詢問個人資料保存同意。
|
`state.json` 保存角色記憶系統狀態,例如 `last_sleep`、`last_sleep_digest`、`last_forget` 與 `personal_memory_consent`。`personal_memory_consent` 只允許 `accepted`/`declined`/`unknown`,供 SessionStart 判斷是否需要再次告知與詢問個人資料保存同意。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user