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
+45 -5
View File
@@ -916,10 +916,26 @@ commands.candidates = ({ flags }) => {
const session = requireSession(flags);
const slug = hostOf(flags, session);
requireOwner(slug, session);
// `--reviewed`:把判斷過的短期記憶標掉。**這是候選數字唯一會往下扣的地方**——
// 固化完不標,同一批下一輪照樣被算成候選,提醒永遠亮著(看起來像睡眠沒做事)。
if (flags.reviewed !== undefined) {
const spec = flags.reviewed === true ? "all" : str(flags.reviewed) || "all";
const indexes = spec === "all" ? null : csv(spec).map(Number);
if (indexes && (!indexes.length || indexes.some((n) => !Number.isInteger(n)))) {
die("`--reviewed` 只能是 `all` 或以逗號分隔的編號(候選輸出裡每筆前面的 `#N`)。");
}
const until = str(flags.until) || null;
const res = pl.markShortTermReviewed(slug, { indexes, until });
const scope = indexes ? `${indexes.length} 筆指名的` : until ? `${until} 之前的` : "全部";
ok(`已標記${scope}短期記憶為判斷過(新標 ${res.marked} 筆,本來就標過 ${res.already} 筆)。`);
const after = pl.promotionCandidates(slug);
say(` 現在還有 ${after.candidates.length} 組候選(短期記憶 ${after.total} 筆,判斷過 ${after.reviewed} 筆)。`);
return;
}
const result = pl.promotionCandidates(slug);
const lines = [
`人格 \`${slug}\`:短期記憶 ${result.total},達固化條件的候選 ${result.candidates.length} ` +
`${result.pressure ? "(已達容量壓力 R6" : ""}`,
`人格 \`${slug}\`:短期記憶 ${result.total}(判斷過 ${result.reviewed} 筆),` +
`達固化條件的候選 ${result.candidates.length}${result.pressure ? "(已達容量壓力 R6" : ""}`,
"轉入條件:",
...pl.PROMOTION_RULES.map((r) => ` ${r.id} ${r.label}`),
];
@@ -938,10 +954,19 @@ commands.candidates = ({ flags }) => {
`\n[${cand.rules.join("+")}] ${cand.kind}${cand.key}」→ 建議 type=${cand.suggested_type}, ` +
`salience=${cand.suggested_salience}${cand.entries.length} 筆依據)`,
);
// `#N` 就是 `--from-short` 與 `--reviewed` 要填的編號
for (const entry of cand.entries.slice(0, 6)) {
lines.push(` - [${entry.role || "?"}] ${String(entry.text || "").slice(0, 90)}(顯著度 ${entry.salience ?? "?"}`);
lines.push(` - #${entry._index} [${entry.role || "?"}] ${String(entry.text || "").slice(0, 90)}(顯著度 ${entry.salience ?? "?"}`);
}
}
if (result.candidates.length) {
lines.push(
"",
"判斷完之後要把來源標掉,不然下一輪同一批又會被算成候選:",
" 固化的  `consolidate --from-short <#N,#N> ...`(寫長期記憶時一起標)",
" 看過不記的`candidates --reviewed <#N,#N>`",
);
}
emit(result, flags.json, lines);
};
@@ -1029,6 +1054,16 @@ commands.consolidate = ({ flags }) => {
];
pl.writeText(file, front.join("\n"));
const total = pl.rebuildIndex(slug);
// 來源短期記憶標成判斷過:不標的話這幾筆下一輪又會被算成固化候選。
// 編號看 `candidates` 輸出裡每筆前面的 `#N`。
if (flags["from-short"] !== undefined) {
const indexes = csv(flags["from-short"]).map((s) => Number(String(s).replace(/^#/, "")));
if (!indexes.length || indexes.some((n) => !Number.isInteger(n))) {
die("`--from-short` 要的是以逗號分隔的編號(候選輸出裡每筆前面的 `#N`)。");
}
const res = pl.markShortTermReviewed(slug, { indexes, promotedTo: name });
say(` 來源的 ${res.marked} 筆短期記憶已標成判斷過(promoted_to: ${name})。`);
}
const forget = num(flags.forget, null);
if (forget !== null) {
// 跟 prune 套同一層保護:承諾、界線與今天的紀錄不能被固化順手洗掉(R4)
@@ -2536,10 +2571,15 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C
睡眠收尾(機械性):關係時間戳→裁短期→收思維導圖→情緒衰減 8 小時→重建索引
→修剪 said→壓縮舊 journal→寫 sleep.json→Gitea 兩區 push+驗證。
預設保留載入鎖(--release 才收工)。--json 只回報「睡完了沒、哪一步出錯」。
candidates --session <id> 列出達到「短期→長期」條件的候選與依據
candidates --session <id> [--reviewed all|<#N,#N> [--until <ISO>]]
列出達到「短期→長期」條件的候選與依據(每筆前面的 #N 就是它的編號)。
--reviewed 把判斷過的短期記憶標掉(看過決定不記的用這個)——
候選數字只有這裡跟 consolidate --from-short 會往下扣,
判斷完不標,同一批下一輪又會被算成候選。
consolidate --session <id> --name <n> --body <b> [--type --about --topics --salience --strength --emotion
--when --where --mood --gist --detail --rules --source --forget]
--when --where --mood --gist --detail --rules --source --forget --from-short <#N,#N>]
內文會切成「主旨/細節」兩層:衰減先吃細節,主旨最後才掉。
--from-short 標記這則是從哪幾筆短期記憶長出來的(順便標成判斷過)。
prune / reindex --session <id>
migrate --session <id> [--all] [--dry-run] [--json]
長期記憶升格式(補 strength、切主旨/細節)。--all 掃所有人格,只回報數量不印內容。