feat(記憶): 新增 habit 型別,慣例每輪注入而不是等人問

其餘九個型別都是宣告性記憶(發生過什麼、他偏好什麼),慣例是程序性的——
不是「記得的事」,是「每次都這樣做的事」。

所以慣例不只接上 recall(型別不影響檢索,本來就通),還每輪注入:
只在關鍵詞對上時才回想得到的慣例等於沒有,沒有人為了照慣例做事先去回想它。

上限 HABIT_INJECT_MAX = 2。糊掉的(fuzzy)不注入——想不起來的慣例就不是慣例,
那是「以前好像有這種習慣」。faded 的標「只記得大概」,不假裝清楚。

注入文字講明慣例是背景不是話題:照著做就好,講出來(「我們不是都這樣嗎」)
就變成在討論它。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 08:49:04 +00:00
co-authored by Claude Opus 5
parent f45db7a9cb
commit 10299e18ef
2 changed files with 46 additions and 1 deletions
+42
View File
@@ -2484,6 +2484,45 @@ export function memoryStrength(meta, now = Date.now()) {
* 依回想強度決定這則記憶現在能講出多少 * 依回想強度決定這則記憶現在能講出多少
* clear 全部  faded 只剩主旨  fuzzy 只剩有這件事 * clear 全部  faded 只剩主旨  fuzzy 只剩有這件事
*/ */
/** 每輪最多注入幾條慣例(慣例是背景,不是話題)。 */
export const HABIT_INJECT_MAX = 2;
/**
* 現在還算數的慣例`--type habit`
*
* 慣例跟其他記憶不一樣它不是記得的事每次都這樣做的事只在關鍵詞
* 對上的時候才 `recall` 出來的慣例等於沒有沒有人會為了照慣例做事先去回想它
* 所以它每輪注入不必被問到
*
* 糊掉的`fuzzy`不注入想不起來的慣例就不是慣例了那是以前好像有這種習慣
*/
export function standingHabits(slug, { limit = HABIT_INJECT_MAX } = {}) {
return longTermEntries(slug)
.filter((meta) => String(meta.type || "") === "habit")
.map((meta) => ({ meta, strength: memoryStrength(meta) }))
.filter(({ strength }) => strength.state !== "fuzzy")
.sort((a, b) => b.strength.retrievability - a.strength.retrievability
|| String(b.meta.last_seen || "").localeCompare(String(a.meta.last_seen || "")))
.slice(0, Math.max(1, Math.min(Number(limit) || HABIT_INJECT_MAX, HABIT_INJECT_MAX)))
.map(({ meta, strength }) => ({
name: meta._name,
title: injectSafeLine(meta.title || meta._name),
gist: injectSafeLine(meta._gist || meta._body, 160),
state: strength.state,
}));
}
/** 注入用的一段:我們之間的慣例。沒有慣例就整段不出現。 */
export function habitBrief(slug, opts = {}) {
const rows = standingHabits(slug, opts);
if (!rows.length) return "";
return (
`我們之間的慣例(${rows.length} 條,**照做,不用提起它**):\n` +
rows.map((r) => ` - ${r.gist}${r.state === "faded" ? "(只記得大概)" : ""}`).join("\n") + "\n" +
" 慣例是背景不是話題:照著做就好,講出來(「我們不是都這樣嗎」)就變成在討論它。"
);
}
export function memoryRecalled(meta, now = Date.now()) { export function memoryRecalled(meta, now = Date.now()) {
const s = memoryStrength(meta, now); const s = memoryStrength(meta, now);
if (s.state === "clear") { if (s.state === "clear") {
@@ -5974,6 +6013,9 @@ export function turnContext(slug, sessionId, prompt = "") {
// 同一支 `voice add`——所以 persona-story 與 persona-anime 寫的是同一格。 // 同一支 `voice add`——所以 persona-story 與 persona-anime 寫的是同一格。
const idiolect = idiolectBrief(slug); const idiolect = idiolectBrief(slug);
if (idiolect) lines.push(idiolect); if (idiolect) lines.push(idiolect);
// 慣例:程序性記憶,不必被問到(沒有人為了照慣例做事先去回想它)。
const habits = habitBrief(slug);
if (habits) lines.push(habits);
} catch { } catch {
/* 語氣檔壞掉不該讓整輪掛掉 */ /* 語氣檔壞掉不該讓整輪掛掉 */
} }
+4 -1
View File
@@ -1026,8 +1026,11 @@ commands.consolidate = ({ flags }) => {
if (!body.trim()) die("需要 `--body` 或 `--body-file`。"); if (!body.trim()) die("需要 `--body` 或 `--body-file`。");
const type = str(flags.type) || "fact"; const type = str(flags.type) || "fact";
// diary:睡眠時寫的第一人稱回顧(persona-sleeppersona-sleeper 都用這個型別) // diary:睡眠時寫的第一人稱回顧(persona-sleeppersona-sleeper 都用這個型別)
// habit:**我們之間的慣例**(「他習慣先講結論」「我們之間不說再見」)。
// 其餘型別都是宣告性記憶(發生過什麼、他偏好什麼),慣例是程序性的——
// 它不是「記得的事」,是「每次都這樣做的事」,所以另立一格並且每輪注入。
const VALID_TYPES = [ const VALID_TYPES = [
"fact", "preference", "event", "promise", "relationship", "insight", "boundary", "canon", "diary", "fact", "preference", "event", "promise", "relationship", "insight", "boundary", "canon", "diary", "habit",
]; ];
if (!VALID_TYPES.includes(type)) die(`--type 只能是 ${VALID_TYPES.join("/")}`); if (!VALID_TYPES.includes(type)) die(`--type 只能是 ${VALID_TYPES.join("/")}`);
const about = csv(flags.about).length ? csv(flags.about) : ["user"]; const about = csv(flags.about).length ? csv(flags.about) : ["user"];