feat(persona 擬真): 記憶會糊掉、情緒有底色與疲勞、人格有懸著的事(0.2.0 發佈) #16

Merged
admin merged 5 commits from develop into master 2026-08-03 06:22:17 +00:00
3 changed files with 1228 additions and 45 deletions
Showing only changes of commit f9b189dd76 - Show all commits
+3
View File
@@ -105,6 +105,9 @@ export const AREAS = {
"state/said.jsonl", "state/said.jsonl",
"state/felt.jsonl", "state/felt.jsonl",
"state/sleep.json", "state/sleep.json",
"state/mood.json",
"state/loops.json",
"state/probe.jsonl",
"memory/short-term.jsonl", "memory/short-term.jsonl",
"memory/inbox/", "memory/inbox/",
"mindmap/threads/", "mindmap/threads/",
+983 -33
View File
File diff suppressed because it is too large Load Diff
+242 -12
View File
@@ -254,6 +254,27 @@ function renderTemplate(name, mapping) {
return text; return text;
} }
/**
* `--tells "anger=不講話;句子只剩動詞|羞愧=摸後頸"` → IDENTITY.md 的 `## Tells` 條目。
*
* 情緒之間用 `|` 分,同一種情緒的多條破口用 `;` 或 `;` 分。
* 沒帶就回空字串:空的區塊等於「這個人格沿用全域預設」,樣板裡的註解會教下一個人怎麼補。
*/
function renderTells(raw) {
const src = String(raw || "").trim();
if (!src) return "";
const lines = [];
for (const group of src.split("|")) {
const idx = group.search(/[=:]/);
if (idx < 0) continue;
const key = group.slice(0, idx).trim();
const tells = group.slice(idx + 1).split(/[;;、,]/).map((s) => s.trim()).filter(Boolean).slice(0, 5);
if (!key || !tells.length) continue;
lines.push(`- ${pl.injectSafeLine(key, 12)}: ${tells.map((t) => pl.injectSafeLine(t, 40)).join("")}`);
}
return lines.join("\n");
}
// --------------------------------------------------------------------------- // // --------------------------------------------------------------------------- //
// 子指令 // 子指令
// --------------------------------------------------------------------------- // // --------------------------------------------------------------------------- //
@@ -296,6 +317,9 @@ commands.create = async ({ flags }) => {
VIBE: str(flags.vibe), VIBE: str(flags.vibe),
EMOJI: str(flags.emoji), EMOJI: str(flags.emoji),
AVATAR: str(flags.avatar), AVATAR: str(flags.avatar),
// 情緒破口:`--tells "anger=不講話;把事情做完再說|羞愧=摸後頸"`。
// 沒帶就留空——空的區塊會退回全域預設,而樣板裡的註解會告訴下一個人怎麼補。
TELLS: renderTells(str(flags.tells)),
CREATED: pl.nowIso(), CREATED: pl.nowIso(),
}; };
for (const filename of ["IDENTITY.md", "SOUL.md", "AGENTS.md", "USER.md"]) { for (const filename of ["IDENTITY.md", "SOUL.md", "AGENTS.md", "USER.md"]) {
@@ -542,7 +566,11 @@ commands.sleep = async ({ flags }) => {
await run("archive-threads", () => pl.archiveStaleThreads(slug)); await run("archive-threads", () => pl.archiveStaleThreads(slug));
await run("emotion-decay", () => { await run("emotion-decay", () => {
pl.updateEmotion(slug, (s) => pl.decayEmotionBy(s, pl.SLEEP_DECAY_MINUTES)); pl.updateEmotion(slug, (s) => pl.decayEmotionBy(s, pl.SLEEP_DECAY_MINUTES));
// 當日底色不歸零,只帶一部分過去——睡一覺不會把昨天的低氣壓抹掉
pl.sleepDayMood(slug);
}); });
// 懸太久沒下文的事在這裡收(睡覺是它自然的收尾點),各留一則短期記憶
await run("sweep-loops", () => ({ cold: pl.sweepLoops(slug).length }));
await run("reindex", () => pl.rebuildIndex(slug)); await run("reindex", () => pl.rebuildIndex(slug));
await run("trim-said", () => pl.trimSaid(slug)); await run("trim-said", () => pl.trimSaid(slug));
await run("archive-journal", () => pl.archiveJournals(slug)); await run("archive-journal", () => pl.archiveJournals(slug));
@@ -743,7 +771,7 @@ commands.remember = ({ flags }) => {
pl.rememberShort(slug, entry); pl.rememberShort(slug, entry);
const kept = pl.pruneShortTerm(slug); const kept = pl.pruneShortTerm(slug);
if (Object.keys(entry.emotion_deltas).length) { if (Object.keys(entry.emotion_deltas).length) {
const state = pl.updateEmotion(slug, (s) => pl.applyEmotion(s, entry.emotion_deltas, text.slice(0, 80))); const state = pl.updateEmotion(slug, (s) => pl.applyEmotion(s, entry.emotion_deltas, text.slice(0, 80), { slug }));
pl.appendJsonl(pl.journalPath(slug), { pl.appendJsonl(pl.journalPath(slug), {
ts: pl.nowIso(), kind: "emotion", trigger: text.slice(0, 120), ts: pl.nowIso(), kind: "emotion", trigger: text.slice(0, 120),
deltas: entry.emotion_deltas, levels: state.levels, mood: pl.mood(state), deltas: entry.emotion_deltas, levels: state.levels, mood: pl.mood(state),
@@ -768,8 +796,18 @@ commands.recall = ({ flags }) => {
const hits = pl.recall(slug, query, limit); const hits = pl.recall(slug, query, limit);
const lines = [`${query}」的長期記憶命中 ${hits.length} 則:`]; const lines = [`${query}」的長期記憶命中 ${hits.length} 則:`];
for (const meta of hits) { for (const meta of hits) {
const first = (meta._body || "").split("\n")[0] || ""; const r = pl.memoryRecalled(meta);
lines.push(`- ${meta._name}${meta.type || "fact"}|顯著度 ${meta.salience ?? "?"}${first.slice(0, 120)}`); const head = `- ${meta._name}${meta.type || "fact"}|顯著度 ${meta.salience ?? "?"}`;
if (r.state === "clear") {
const first = (meta._gist || meta._body || "").split("\n")[0] || "";
lines.push(`${head}${first.slice(0, 120)}`);
} else if (r.state === "faded") {
lines.push(`${head}|⚠ 半模糊 ${Math.round(r.retrievability * 100)}%|主旨:${r.gist.split("\n")[0].slice(0, 100)}`);
lines.push(` ${r.hint}`);
} else {
lines.push(`${head}|⚠ 模糊 ${Math.round(r.retrievability * 100)}%|只剩線索:${(r.topics || []).join("/") || "-"}`);
lines.push(` ${r.hint}`);
}
} }
const recents = pl.recentShort(slug, limit); const recents = pl.recentShort(slug, limit);
if (recents.length) { if (recents.length) {
@@ -782,7 +820,8 @@ commands.recall = ({ flags }) => {
lines.push("心裡話(只有你自己看得到,不要講給他聽):"); lines.push("心裡話(只有你自己看得到,不要講給他聽):");
for (const row of inner) lines.push(`- 💭 [${row.kind || "infer"}] ${String(row.text || "").slice(0, 110)}`); for (const row of inner) lines.push(`- 💭 [${row.kind || "infer"}] ${String(row.text || "").slice(0, 110)}`);
} }
pl.touchRecall(slug, hits.map((m) => m._name)); // 模糊到只剩線索的那幾則**不算被想起來**:真的想不起來就不該重設它的衰減
pl.touchRecall(slug, hits.filter((m) => (m._recall?.retrievability ?? 1) >= pl.MEMORY_FUZZY_AT).map((m) => m._name));
emit({ persona: slug, long_term: hits, short_term: recents, inner }, flags.json, lines); emit({ persona: slug, long_term: hits, short_term: recents, inner }, flags.json, lines);
}; };
@@ -850,12 +889,23 @@ commands.said = ({ flags, positional }) => {
for (const issue of blockers) lines.push(`${pl.speechLintMessage(issue)}`); for (const issue of blockers) lines.push(`${pl.speechLintMessage(issue)}`);
// hint 不算不通過(例如講到自己的過去),但要提醒去驗證 // hint 不算不通過(例如講到自己的過去),但要提醒去驗證
for (const issue of lint.filter((i) => i.level === "hint")) lines.push(`· ${pl.speechLintMessage(issue)}`); for (const issue of lint.filter((i) => i.level === "hint")) lines.push(`· ${pl.speechLintMessage(issue)}`);
// 模糊態的兩種句型:試探是**提醒**(記一筆),假裝記得是**提醒去查**。
// 這裡刻意都不擋——擋下去等於把「開放試探」那條界線收回來,稽核才是煞車。
const probing = pl.looksLikeProbe(text);
const asserting = pl.looksLikeFabrication(text);
if (probing) {
lines.push("· 這句是試探:說出口之後記一筆 `persona.mjs probe add --text \"...\"`" +
"對方回了再 `probe confirm` 或 `probe deny`。");
} else if (asserting) {
lines.push("· 這句在斷言自己的過去,而且沒有在問:先 `recall` 確認那則記憶還清晰。" +
"只要它是模糊態,就改成帶問號的試探句,不可以把細節補出來。");
}
const clean = !repeat && !tooLong && !blockers.length; const clean = !repeat && !tooLong && !blockers.length;
if (clean) { if (clean) {
lines.push(`✔ 沒講過,${sentences} 句,可以說。`); lines.push(`✔ 沒講過,${sentences} 句,可以說。`);
if (flags.record) pl.recordSaid(slug, text, { kind: "reply" }); if (flags.record) pl.recordSaid(slug, text, { kind: "reply" });
} }
emit({ persona: slug, repeat, sentences, lint, ok: clean }, flags.json, lines); emit({ persona: slug, repeat, sentences, lint, probing, asserting, ok: clean }, flags.json, lines);
return; return;
} }
die(`未知 action${action}(可用 check/list`); die(`未知 action${action}(可用 check/list`);
@@ -930,6 +980,13 @@ commands.consolidate = ({ flags }) => {
"fact", "preference", "event", "promise", "relationship", "insight", "boundary", "canon", "diary", "fact", "preference", "event", "promise", "relationship", "insight", "boundary", "canon", "diary",
]; ];
if (!VALID_TYPES.includes(type)) die(`--type 只能是 ${VALID_TYPES.join("/")}`); if (!VALID_TYPES.includes(type)) die(`--type 只能是 ${VALID_TYPES.join("/")}`);
// 情境戳章與主旨/細節切分:兩者都可以用旗標明寫,沒寫就自動推。
const stamp = pl.contextStamp(slug);
// `--gist` 明寫時,`--detail` 沒給就是**沒有細節**——退回整份 body 會讓主旨之外
// 再抄一次全文,那是把「衰減先吃細節」這件事整個抵銷掉。
const splitBody = str(flags.gist)
? { gist: str(flags.gist), detail: str(flags.detail) }
: pl.splitGistDetail(body);
const about = csv(flags.about).length ? csv(flags.about) : ["user"]; const about = csv(flags.about).length ? csv(flags.about) : ["user"];
// `about` 是自由字串(原樣保留);對得上關係圖節點的才多寫一行 id,之後 recall 與 // `about` 是自由字串(原樣保留);對得上關係圖節點的才多寫一行 id,之後 recall 與
// 「人際關係」那段才知道講的是同一個人。一個都對不上就不輸出這一行。 // 「人際關係」那段才知道講的是同一個人。一個都對不上就不輸出這一行。
@@ -949,7 +1006,16 @@ commands.consolidate = ({ flags }) => {
...(aboutIds.length ? [`about_ids: [${aboutIds.join(", ")}]`] : []), ...(aboutIds.length ? [`about_ids: [${aboutIds.join(", ")}]`] : []),
`topics: [${csv(flags.topics).map(fm).filter(Boolean).join(", ")}]`, `topics: [${csv(flags.topics).map(fm).filter(Boolean).join(", ")}]`,
`salience: ${num(flags.salience, 60)}`, `salience: ${num(flags.salience, 60)}`,
// 回想強度:連續衰減的起點。已經存在的記憶保留自己長出來的強度,
// 不要因為重寫一次就把「被想起過很多次」的歷史抹平。
`strength: ${num(flags.strength, Number(existing.strength ?? pl.MEMORY_STRENGTH_DEFAULT))}`,
`emotion: ${fm(str(flags.emotion) || "none")}`, `emotion: ${fm(str(flags.emotion) || "none")}`,
// 情境索引:什麼時候、在哪裡、什麼心情記下來的(回想時的非語意線索)。
// 推不出來的欄位就不寫——全專案一致:沒有值不留空欄位。
...["when", "where", "mood"]
.map((k) => [k, fm(str(flags[k]) || existing[k] || stamp[k] || "")])
.filter(([, v]) => v)
.map(([k, v]) => `${k}: ${v}`),
`rules: ${fm(str(flags.rules) || "manual")}`, `rules: ${fm(str(flags.rules) || "manual")}`,
`first_seen: ${existing.first_seen || today}`, `first_seen: ${existing.first_seen || today}`,
`last_seen: ${today}`, `last_seen: ${today}`,
@@ -957,7 +1023,8 @@ commands.consolidate = ({ flags }) => {
`source: ${fm(str(flags.source) || "short-term")}`, `source: ${fm(str(flags.source) || "short-term")}`,
"---", "---",
"", "",
body.trim(), // 主旨/細節兩層:衰減先吃細節,主旨最後才掉。沒有明寫就用第一段當主旨。
pl.joinGistDetail(splitBody.gist, splitBody.detail),
"", "",
]; ];
pl.writeText(file, front.join("\n")); pl.writeText(file, front.join("\n"));
@@ -984,6 +1051,139 @@ commands.prune = ({ flags }) => {
ok(`短期記憶已裁剪,剩 ${kept} 筆(保留上限 ${pl.SHORT_TERM_KEEP} 筆 / ${pl.SHORT_TERM_DAYS} 天)。`); ok(`短期記憶已裁剪,剩 ${kept} 筆(保留上限 ${pl.SHORT_TERM_KEEP} 筆 / ${pl.SHORT_TERM_DAYS} 天)。`);
}; };
/**
* 把長期記憶檔升到新格式(`strength` 與主旨/細節兩層)。
*
* `--all` 會掃過**所有**人格——這是唯一一個跨人格的維護指令,
* 所以它只回報「動了幾個檔」,一個字的內容都不印出來(跨人格隔離的界線在這裡不能鬆)。
*/
commands.migrate = ({ flags }) => {
const session = requireSession(flags);
const dryRun = Boolean(flags["dry-run"]);
const targets = flags.all ? pl.listPersonas() : [hostOf(flags, session)];
if (!flags.all) requireOwner(targets[0], session);
const rows = [];
for (const slug of targets) {
const stats = pl.migrateLongTerm(slug, { dryRun });
if (!dryRun && (stats.strength || stats.split)) pl.rebuildIndex(slug);
rows.push({ persona: slug, ...stats });
}
const lines = [dryRun ? "長期記憶格式遷移(試跑,沒有寫入):" : "長期記憶格式遷移完成:"];
for (const r of rows) {
lines.push(`- ${r.persona}|共 ${r.total} 則|補 strength ${r.strength}|切主旨/細節 ${r.split}` +
(r.skipped ? `|讀寫失敗 ${r.skipped}` : ""));
}
lines.push(" (只回報數量:跨人格維護不印任何記憶內容。)");
emit({ dry_run: dryRun, personas: rows }, flags.json, lines);
};
// --------------------------------------------------------------------------- //
// loop:懸著的事(未完事項)
// --------------------------------------------------------------------------- //
commands.loop = ({ flags, positional }) => {
const session = requireSession(flags);
const slug = hostOf(flags, session);
const action = (positional[0] || "list").toLowerCase();
const show = () => {
const live = pl.openLoops(slug);
const lines = [`\`${slug}\` 現在懸著 ${live.length}/${pl.LOOP_MAX} 件事:`];
for (const l of live) {
const days = Math.floor((Date.now() - (pl.parseIso(l.opened_at)?.getTime() ?? Date.now())) / 86_400_000);
lines.push(`- ${l.id}${pl.LOOP_KINDS[l.kind] || l.kind}${l.text}(開了 ${days} 天)`);
}
if (!live.length) lines.push("- (沒有懸著的事)");
emit({ persona: slug, loops: live }, flags.json, lines);
};
if (action === "list") {
requireMember(slug, session, Boolean(flags["as-guest"]));
show();
return;
}
requireOwner(slug, session);
if (action === "add") {
const res = pl.addLoop(slug, { text: str(flags.text), kind: str(flags.kind) || "topic", about: str(flags.about) });
if (!res.ok) die(`開不了:${res.reason}`);
ok(`懸著了:${res.loop.id}${pl.LOOP_KINDS[res.loop.kind]}${res.loop.text}${res.open}/${pl.LOOP_MAX}`);
return;
}
if (action === "done" || action === "drop" || action === "cold") {
const id = str(flags.id);
if (!id) die("需要 `--id`(用 `loop list` 查)。");
const res = pl.closeLoop(slug, id, action === "done" ? "done" : action, str(flags.note));
if (!res.ok) die(res.reason);
ok(`收掉了 ${res.loop.id}${res.loop.text}`);
return;
}
if (action === "touch") {
const id = str(flags.id);
if (!id) die("需要 `--id`。");
const res = pl.touchLoop(slug, id);
if (!res.ok) die(res.reason);
ok(`${res.loop.id} 有進展,逾期重算。`);
return;
}
if (action === "sweep") {
const cold = pl.sweepLoops(slug);
ok(cold.length
? `收掉 ${cold.length} 件沒下文的(各留了一則短期記憶):${cold.map((l) => l.text).join("、")}`
: `沒有超過 ${pl.LOOP_STALE_DAYS} 天沒進展的。`);
return;
}
die("用法:`loop add|done|drop|touch|sweep|list`。");
};
// --------------------------------------------------------------------------- //
// probe:模糊記憶的試探與稽核(開放界線唯一的煞車)
// --------------------------------------------------------------------------- //
commands.probe = ({ flags, positional }) => {
const session = requireSession(flags);
const slug = hostOf(flags, session);
const action = (positional[0] || "audit").toLowerCase();
if (action === "audit") {
requireMember(slug, session, Boolean(flags["as-guest"]));
const a = pl.probeAudit(slug, num(flags.limit, 50));
const lines = [
`\`${slug}\` 的試探稽核(最近 ${a.total} 次):`,
` 確認 ${a.confirmed}/否認 ${a.denied}/還沒結案 ${a.pending}`,
a.denied_ratio === null
? " 還沒有結案的試探,算不出錯誤率。"
: ` 結案的裡面有 ${a.denied_ratio}% 被否認` +
(a.denied_ratio >= 40 ? " ⚠ 太高了:模糊態正在被拿來編內容,先收緊到「只能說記不清」。" : "。"),
];
for (const row of a.recent) lines.push(` - [${row.outcome}] ${row.text}${row.memory ? `${row.memory}` : ""}`);
emit({ persona: slug, audit: a }, flags.json, lines);
return;
}
requireOwner(slug, session);
if (action === "add") {
const text = str(flags.text);
if (!text) die("需要 `--text`(你實際問出口的那句試探)。");
if (!pl.looksLikeProbe(text)) {
die("這句不像試探:試探一定要在**問**(帶問號,例如「是不是上個月那次?」)。\n" +
" 可以說不確定,不可以斷言——斷言就是幻覺,不是模糊記憶。");
}
const entry = pl.recordProbe(slug, { text, memory: str(flags.memory) });
ok(`記下這次試探了:${entry.text} → 對方回了之後用 \`probe confirm\`\`probe deny\``);
return;
}
if (action === "confirm" || action === "deny") {
const hit = pl.resolveProbe(slug, action === "confirm" ? "confirmed" : "denied", {
at: str(flags.id) || null, // 不指定就結最近一筆還沒結案的
note: str(flags.note),
});
if (!hit) die("沒有還沒結案的試探。");
if (action === "deny") {
ok(`記成「被否認」了:${hit.text}\n 現在就寫一則更正記憶(\`remember\`\`consolidate\`),不要放著。`);
} else {
ok(`記成「對方確認」了:${hit.text}`);
}
return;
}
die("用法:`probe add|confirm|deny|audit`。");
};
commands.reindex = ({ flags }) => { commands.reindex = ({ flags }) => {
const session = requireSession(flags); const session = requireSession(flags);
const slug = hostOf(flags, session); const slug = hostOf(flags, session);
@@ -1045,7 +1245,7 @@ commands.emotion = ({ flags }) => {
deltas = rel.gain === 1 deltas = rel.gain === 1
? raw ? raw
: Object.fromEntries(Object.entries(raw).map(([k, v]) => [k, Math.round(v * rel.gain * 100) / 100])); : Object.fromEntries(Object.entries(raw).map(([k, v]) => [k, Math.round(v * rel.gain * 100) / 100]));
s = pl.applyEmotion(s, deltas, str(flags.trigger)); s = pl.applyEmotion(s, deltas, str(flags.trigger), { slug });
if (rel.gain !== 1) { if (rel.gain !== 1) {
s.last_trigger = s.last_trigger || {}; s.last_trigger = s.last_trigger || {};
s.last_trigger.relation_gain = { who: rel.name, closeness: rel.closeness, gain: rel.gain }; s.last_trigger.relation_gain = { who: rel.name, closeness: rel.closeness, gain: rel.gain };
@@ -1058,10 +1258,10 @@ commands.emotion = ({ flags }) => {
pl.recordFelt(slug, { mine: state.last_trigger?.deltas || null, note: str(flags.trigger) }); pl.recordFelt(slug, { mine: state.last_trigger?.deltas || null, note: str(flags.trigger) });
pl.appendJsonl(pl.journalPath(slug), { pl.appendJsonl(pl.journalPath(slug), {
ts: pl.nowIso(), kind: "emotion", trigger: str(flags.trigger), ts: pl.nowIso(), kind: "emotion", trigger: str(flags.trigger),
deltas, applied: state.last_trigger?.deltas || {}, levels: state.levels, mood: pl.mood(state), deltas, applied: state.last_trigger?.deltas || {}, levels: state.levels, mood: pl.moodOf(slug, state),
}); });
} }
const m = pl.mood(state); const m = pl.moodOf(slug, state);
const row = (key) => const row = (key) =>
` ${pl.EMOTIONS[key].zh} ${key.padEnd(13)}${String(state.levels[key]).padStart(6)}(基線 ${state.baseline[key]}`; ` ${pl.EMOTIONS[key].zh} ${key.padEnd(13)}${String(state.levels[key]).padStart(6)}(基線 ${state.baseline[key]}`;
const lines = [ const lines = [
@@ -1693,6 +1893,23 @@ commands.import = ({ flags }) => {
]; ];
if (result.rejected.length) lines.push(` ⚠ 略過 ${result.rejected.length} 個路徑不合法的項目:${result.rejected.slice(0, 3).join(", ")}`); if (result.rejected.length) lines.push(` ⚠ 略過 ${result.rejected.length} 個路徑不合法的項目:${result.rejected.slice(0, 3).join(", ")}`);
if (!checksumOk) lines.push(" ⚠ checksum 不符(--force 略過):內容可能被改過,請自行確認。"); if (!checksumOk) lines.push(" ⚠ checksum 不符(--force 略過):內容可能被改過,請自行確認。");
// 舊 bundle(v1)匯進來會自動升格式。這件事以前只出現在 --json 裡,
// 等於人看的輸出完全沒提——升成功要講,升失敗更要講。
const inVersion = Number(bundle.version) || 1;
if (inVersion < pl.BUNDLE_VERSION) {
if (result.migrated) {
lines.push(
` 這是 v${inVersion} 的舊 bundle,已就地升到 v${pl.BUNDLE_VERSION}` +
`${result.migrated.total} 則長期記憶,補 strength ${result.migrated.strength}、切主旨/細節 ${result.migrated.split}` +
(result.migrated.skipped ? `${result.migrated.skipped} 則看不懂格式沒動` : "") + "。",
);
} else {
lines.push(
` ⚠ 這是 v${inVersion} 的舊 bundle,但自動升格式失敗了。` +
"請手動跑 `persona.mjs migrate --dry-run` 看情況,再決定要不要 `migrate`。",
);
}
}
if (flags.load) { if (flags.load) {
if (data.host && data.host !== result.persona) { if (data.host && data.host !== result.persona) {
lines.push(` ⚠ 本 session 已載入 \`${data.host}\`,未自動載入;要用它請先 release。`); lines.push(` ⚠ 本 session 已載入 \`${data.host}\`,未自動載入;要用它請先 release。`);
@@ -2297,7 +2514,9 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C
人格與鎖: 人格與鎖:
create --romaji <英文名> --session <id> [--persona <目錄名> --code <ASUNA-01> --name --creature --gender create --romaji <英文名> --session <id> [--persona <目錄名> --code <ASUNA-01> --name --creature --gender
--vibe --emoji --avatar --baseline --origin --work --no-gitea --public] --vibe --emoji --avatar --tells --baseline --origin --work --no-gitea --public]
--tells "anger=不講話;句子只剩動詞|羞愧=摸後頸" 情緒破口,寫進 IDENTITY.md 的
## Tells 區塊(蓋掉全域預設;沒帶就沿用預設)
人格編號 = 英文名全大寫 + 兩位索引(同名才遞增),也是 Gitea 存取庫的名稱; 人格編號 = 英文名全大寫 + 兩位索引(同名才遞增),也是 Gitea 存取庫的名稱;
不指定 --persona 就用編號當目錄名。 不指定 --persona 就用編號當目錄名。
load --persona <slug> --session <id> [--takeover] load --persona <slug> --session <id> [--takeover]
@@ -2318,15 +2537,26 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C
→修剪 said→壓縮舊 journal→寫 sleep.json→Gitea 兩區 push+驗證。 →修剪 said→壓縮舊 journal→寫 sleep.json→Gitea 兩區 push+驗證。
預設保留載入鎖(--release 才收工)。--json 只回報「睡完了沒、哪一步出錯」。 預設保留載入鎖(--release 才收工)。--json 只回報「睡完了沒、哪一步出錯」。
candidates --session <id> 列出達到「短期→長期」條件的候選與依據 candidates --session <id> 列出達到「短期→長期」條件的候選與依據
consolidate --session <id> --name <n> --body <b> [--type --about --topics --salience --emotion --rules --source --forget] consolidate --session <id> --name <n> --body <b> [--type --about --topics --salience --strength --emotion
--when --where --mood --gist --detail --rules --source --forget]
內文會切成「主旨/細節」兩層:衰減先吃細節,主旨最後才掉。
prune / reindex --session <id> prune / reindex --session <id>
migrate --session <id> [--all] [--dry-run] [--json]
長期記憶升格式(補 strength、切主旨/細節)。--all 掃所有人格,只回報數量不印內容。
loop add|done|drop|touch|sweep|list --session <id> [--text --kind question|promise|topic|mine --id --note]
懸著的事(同時最多 5 條):他沒回答的問題/他答應要做的事/被打斷的話題/我想問但沒問的。
7 天沒進展自動收掉並留一則「沒下文」。mine 那種同時是自我議程的來源。
probe add|confirm|deny|audit --session <id> [--text --memory --note --id --limit]
模糊記憶的試探紀錄:可以說不確定、可以問,**不可以斷言**(add 會擋掉沒問號的句子)。
audit 看「試探幾次、被否認幾次」——這是開放這條界線唯一的煞車。
情緒與圖: 情緒與圖:
emotion --session <id> [--apply joy=+10,...] [--baseline ...] [--trigger <why>] emotion --session <id> [--apply joy=+10,...] [--baseline ...] [--trigger <why>]
[--from <對象>] 這件事是誰引起的:親近度會放大/縮小 delta [--from <對象>] 這件事是誰引起的:親近度會放大/縮小 delta
[--read "<對方說的話>"] 只讀情緒訊號與該怎麼接,不改狀態 [--read "<對方說的話>"] 只讀情緒訊號與該怎麼接,不改狀態
[--audit [--limit 20]] 最近幾輪的 delta 是不是只往一邊倒+對方的走向 [--audit [--limit 20]] 最近幾輪的 delta 是不是只往一邊倒+對方的走向
套用時會飽和(越接近端點漲越慢)並受單輪預算限制(|delta| 總和 ≤ 40 套用時會飽和受單輪預算限制(|delta| 總和 ≤ 60,再經交互抑制、慣性、
當日底色與疲勞調整——實際生效的值看 last_trigger.scaled
mindmap show|thread|list --session <id> [--topic <t>] [--force] mindmap show|thread|list --session <id> [--topic <t>] [--force]
relation node|edge|render|show --session <id> [--name --id --kind --bond --closeness --trust --note --tags --from --to --label --affinity] relation node|edge|render|show --session <id> [--name --id --kind --bond --closeness --trust --note --tags --from --to --label --affinity]
relation style --session <id> --name <who> [--facet 稱呼 --value 親愛的 --except anger>=40 --since --clear] relation style --session <id> --name <who> [--facet 稱呼 --value 親愛的 --except anger>=40 --since --clear]