feat(modesty): 羞恥度動態化+三種出口(縮/炸/坦白),心裡話進得了記憶(v0.0.8 同批)

一、羞恥度高不等於話變少——三個出口(他指出來的,我上一版只做對一半)
  縮 shrink   :預設。一句嘴硬,說出口的那句在**迴避**心裡那句
  炸 spill    :慌了(焦慮 ≥55)/惱羞(憤怒 ≥45)/對方在生氣或逼你澄清
                → **4 句,但單句只有 22 字**:掩飾、急著否認、硬轉話題,碎而急
  坦白 confess:羞恥高+信任 ≥75+只有兩個人 → 3 句完整句,憋很久一次講完,
                而且**先寫完心裡話再開口**
  關鍵:句數與單句字數要**一起動**,只調句數分不出「碎念」與「演講」。
  `room post` 本來就同時擋這兩個,參數一改就生效。

二、羞恥度動態化(模擬跑過才實作)
  `modestyState()` = trait + 情緒推力 + 語氣層 + 上一輪餘溫,存進 `felt.jsonl`。
  護欄四道:`MODESTY_GAIN`=0.35(<1 才收斂,0.9 三輪就貼 100)、一輪最多動 15、
  推力上限 25、只算超出基線的部分(比較不開心不代表比較害羞)。
  慢的情緒推力打折(信任半衰期 720 分是底色,不是此刻的事)——
  沒有這條,桐人會因為長期信任高而永遠低於自己的 trait 二十分。
  斷路器是惱羞成怒:anger 權重是負的,被逗到極限翻臉,羞恥自己掉下來。

三、心裡話進得了記憶(但永遠不回顯)
  在這之前心裡話只進 `inner.jsonl`、只被注入最近三句,**永遠不會變成記憶**。
  對害羞的人格來說那是致命的——重要的東西幾乎都在心裡那句。
  現在:`recallInner()` 讓 `recall` 找得到(標「不要講給他聽」),
  `innerCandidates()` 把「24 小時內想過 ≥2 次的同一件事」列成固化候選。
  要不要固化仍由人格自己決定。`think` 的輸出還是只有「💭 心想 N 句」。

四、修掉一個踩到的坑
  `Number(null) === 0` —— 沒有上一輪時 `prev` 退回了 0 而不是 trait,
  害每個人格的羞恥度都從 0 起算。已加測試守著。

測試:328 項全過(新增 14 項)。切詞從貪婪的 {2,4} 改成兩字滑動視窗——
貪婪比對會把「那台舊鐘的齒輪」切成固定區塊,「齒輪」永遠不會單獨出現。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 06:12:18 +00:00
co-authored by Claude Opus 5
parent 5b52dd18c2
commit ab4ddb734d
6 changed files with 320 additions and 43 deletions
+16 -1
View File
@@ -744,8 +744,14 @@ commands.recall = ({ flags }) => {
lines.push("短期記憶(最近):");
for (const row of recents) lines.push(`- [${row.role || "?"}] ${String(row.text || "").slice(0, 110)}`);
}
// 心裡話也找得到——害羞的人格,重要的東西幾乎都在心裡那句
const inner = pl.recallInner(slug, query, limit);
if (inner.length) {
lines.push("心裡話(只有你自己看得到,不要講給他聽):");
for (const row of inner) lines.push(`- 💭 [${row.kind || "infer"}] ${String(row.text || "").slice(0, 110)}`);
}
pl.touchRecall(slug, hits.map((m) => m._name));
emit({ persona: slug, long_term: hits, short_term: recents }, flags.json, lines);
emit({ persona: slug, long_term: hits, short_term: recents, inner }, flags.json, lines);
};
/**
@@ -836,6 +842,15 @@ commands.candidates = ({ flags }) => {
...pl.PROMOTION_RULES.map((r) => ` ${r.id} ${r.label}`),
];
if (!result.candidates.length) lines.push("目前沒有需要固化的內容(未達任何條件)。");
// 心裡話裡反覆出現的念頭:想過好幾次的事,多半是真的重要
const thoughts = pl.innerCandidates(slug);
if (thoughts.length) {
lines.push(`\n心裡話裡一直在想的事(${thoughts.length} 組,24 小時內)——要不要記住由你決定:`);
for (const g of thoughts) {
lines.push(`${g.token}」想過 ${g.count}`);
for (const e of g.entries.slice(-2)) lines.push(` - 💭 ${String(e.text || "").slice(0, 80)}`);
}
}
for (const cand of result.candidates) {
lines.push(
`\n[${cand.rules.join("+")}] ${cand.kind}${cand.key}」→ 建議 type=${cand.suggested_type}, ` +