fix(記憶固化): 判斷過的短期記憶留痕跡,候選數字才會降

睡眠有做固化,但隔天開機照樣提醒「N 組已達固化條件」,數字只會往上爬
(實測 KIRITO-01:短期記憶 120 筆、候選 222 組)。原因不是睡眠沒做事,
是判斷沒有留下痕跡——promotionCandidates() 無條件掃全部短期記憶,而
consolidate 不在來源那筆上寫任何東西,也不刪它(--forget 是按顯著度刪,
不是按固化過沒有刪),所以同一批每輪都被重算成候選。

- 短期記憶多兩個欄位:reviewed_at(看過、判斷過了)與 promoted_to
  (固化成了哪一則)。兩者分開記——「看過決定不記」跟「已經記下來」都不該
  再進候選,但事後要查「這則長期記憶從哪幾筆長出來」只能靠 promoted_to。
- promotionCandidates() 只看沒有 reviewed_at 的那些;total 照舊算全部。
  R6 容量壓力仍看總筆數,但候選只從未判斷的挑,全部判斷完就不再出現
  (否則它會單獨把提醒永遠點亮)。
- consolidate --from-short "#N,#N":寫長期記憶時一起標來源。
- candidates --reviewed all|<#N,#N> [--until <ISO>]:看過決定不記的標這裡。
  --until 讓「某次睡眠當下判斷過的那批」可以一次收掉。
- candidates 輸出每筆前面加 #N(就是上面兩個旗標要填的編號),並在結尾
  提示判斷完要標記。
- 標記不等於刪掉:那幾筆還在短期記憶裡,照舊被 prune 依天數與顯著度裁。

文件:persona-sleep 的流程多一步「標掉判斷過的」並註明漏掉的後果、
persona-memory 補一張「怎麼標」的表、README 的 R1–R6 段補這一層。
selftest 補 13 項(602 → 615 全綠)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 07:12:50 +00:00
co-authored by Claude Opus 5
parent 0917e905c7
commit b5c540feb1
6 changed files with 192 additions and 21 deletions
+46
View File
@@ -293,6 +293,52 @@ check("candidates 子指令可輸出 JSON", (() => {
}
})(), candOut.stdout.slice(0, 120));
// 判斷過的不再重算成候選。以前這裡是無條件掃全部短期記憶,固化完也不留痕跡,
// 所以同一批每輪都被算成候選,提醒的數字只會往上爬——看起來像睡眠沒做固化。
{
const before = pl.promotionCandidates("alpha");
check("候選會回報判斷過幾筆", typeof before.reviewed === "number" && before.reviewed === 0);
const target = before.candidates.find((c) => c.kind === "entry");
const idx = target.entries[0]._index;
check("候選每筆都帶 _index--from-short--reviewed 用的編號)", Number.isInteger(idx));
const consol = cli(["consolidate", "--session", S_HOST, "--name", "reviewed-source", "--body", "從短期記憶長出來的一則",
"--type", "event", "--salience", "60", "--from-short", String(idx)]);
check("consolidate --from-short 會回報標了幾筆", consol.stdout.includes("已標成判斷過"), consol.stdout.slice(-160));
const rowsAfter = pl.readJsonl(pl.shortTermPath("alpha"));
check("來源短期記憶留下 reviewed_at 與 promoted_to",
Boolean(rowsAfter[idx].reviewed_at) && rowsAfter[idx].promoted_to === "reviewed-source",
JSON.stringify(rowsAfter[idx]).slice(0, 160));
const mid = pl.promotionCandidates("alpha");
check("固化過的那筆不再是候選", !mid.candidates.some((c) => c.entries.some((e) => e._index === idx)));
check("固化後判斷過的筆數往上加、總筆數不變", mid.reviewed === 1 && mid.total === before.total);
// 看過決定不記的那些:候選數字唯一另一個往下扣的地方
const rev = cli(["candidates", "--session", S_HOST, "--reviewed", "all"]);
check("candidates --reviewed all 會標掉全部", rev.stdout.includes("已標記全部短期記憶為判斷過"), rev.stdout.slice(0, 160));
const after = pl.promotionCandidates("alpha");
check("全部判斷過之後候選歸零", after.candidates.length === 0, JSON.stringify(after.candidates.map((c) => c.key)));
check("判斷過不等於刪掉(短期記憶筆數不變)", after.total === before.total);
check("R6 容量壓力不會單獨把提醒點亮", (() => {
const rows = pl.readJsonl(pl.shortTermPath("alpha"));
return rows.length >= pl.CONSOLIDATE_THRESHOLD ? after.candidates.length === 0 : true;
})());
// 重標一次不會把時間戳往後推(第一次判斷的時間才是真的)
const firstAt = pl.readJsonl(pl.shortTermPath("alpha"))[idx].reviewed_at;
cli(["candidates", "--session", S_HOST, "--reviewed", "all"]);
check("重標不會覆蓋原本的 reviewed_at", pl.readJsonl(pl.shortTermPath("alpha"))[idx].reviewed_at === firstAt);
// 新寫進來的一筆照樣是候選:標記只擋已經判斷過的,不是把規則關掉
cli(["remember", "--session", S_HOST, "--role", "user", "--text", "他說明天早上要一起去看那台車",
"--topics", "car", "--salience", "70"]);
check("標記過之後新的短期記憶照樣成為候選", pl.promotionCandidates("alpha").candidates.length > 0);
// --until 只標時間之前的:睡眠當下判斷過的那批可以一次收掉,之後寫的不受影響
const marked = pl.markShortTermReviewed("alpha", { until: "2000-01-01T00:00:00Z" });
check("--until 不會標到時間之後的紀錄", marked.marked === 0);
}
console.log("⑦ 心智圖 / 思維導圖 / 人際關係圖");
cli(["mindmap", "thread", "--persona", "alpha", "--session", S_HOST, "--topic", "壓力來源"]);
check("思維導圖建立(Mermaid graph",