diff --git a/scripts/persona-lib.mjs b/scripts/persona-lib.mjs index 9837644..7ea0324 100644 --- a/scripts/persona-lib.mjs +++ b/scripts/persona-lib.mjs @@ -2484,6 +2484,45 @@ export function memoryStrength(meta, now = Date.now()) { * 依回想強度決定「這則記憶現在能講出多少」。 * 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()) { const s = memoryStrength(meta, now); if (s.state === "clear") { @@ -5974,6 +6013,9 @@ export function turnContext(slug, sessionId, prompt = "") { // 同一支 `voice add`——所以 persona-story 與 persona-anime 寫的是同一格。 const idiolect = idiolectBrief(slug); if (idiolect) lines.push(idiolect); + // 慣例:程序性記憶,不必被問到(沒有人為了照慣例做事先去回想它)。 + const habits = habitBrief(slug); + if (habits) lines.push(habits); } catch { /* 語氣檔壞掉不該讓整輪掛掉 */ } diff --git a/scripts/persona.mjs b/scripts/persona.mjs index 0006d7b..79373c7 100644 --- a/scripts/persona.mjs +++ b/scripts/persona.mjs @@ -1026,8 +1026,11 @@ commands.consolidate = ({ flags }) => { if (!body.trim()) die("需要 `--body` 或 `--body-file`。"); const type = str(flags.type) || "fact"; // diary:睡眠時寫的第一人稱回顧(persona-sleep/persona-sleeper 都用這個型別) + // habit:**我們之間的慣例**(「他習慣先講結論」「我們之間不說再見」)。 + // 其餘型別都是宣告性記憶(發生過什麼、他偏好什麼),慣例是程序性的—— + // 它不是「記得的事」,是「每次都這樣做的事」,所以另立一格並且每輪注入。 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("/")}。`); const about = csv(flags.about).length ? csv(flags.about) : ["user"];