From 10299e18ef8e2d56d110744fc0ba814defdaf57d Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 3 Aug 2026 08:49:04 +0000 Subject: [PATCH] =?UTF-8?q?feat(=E8=A8=98=E6=86=B6):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20habit=20=E5=9E=8B=E5=88=A5=EF=BC=8C=E6=85=A3=E4=BE=8B?= =?UTF-8?q?=E6=AF=8F=E8=BC=AA=E6=B3=A8=E5=85=A5=E8=80=8C=E4=B8=8D=E6=98=AF?= =?UTF-8?q?=E7=AD=89=E4=BA=BA=E5=95=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 其餘九個型別都是宣告性記憶(發生過什麼、他偏好什麼),慣例是程序性的—— 不是「記得的事」,是「每次都這樣做的事」。 所以慣例不只接上 recall(型別不影響檢索,本來就通),還每輪注入: 只在關鍵詞對上時才回想得到的慣例等於沒有,沒有人為了照慣例做事先去回想它。 上限 HABIT_INJECT_MAX = 2。糊掉的(fuzzy)不注入——想不起來的慣例就不是慣例, 那是「以前好像有這種習慣」。faded 的標「只記得大概」,不假裝清楚。 注入文字講明慣例是背景不是話題:照著做就好,講出來(「我們不是都這樣嗎」) 就變成在討論它。 Co-Authored-By: Claude Opus 5 (1M context) --- scripts/persona-lib.mjs | 42 +++++++++++++++++++++++++++++++++++++++++ scripts/persona.mjs | 5 ++++- 2 files changed, 46 insertions(+), 1 deletion(-) 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"];