feat(persona 擬真): 記憶會糊掉、情緒有底色與疲勞、人格有懸著的事(0.2.0 發佈) #16
@@ -105,6 +105,9 @@ export const AREAS = {
|
||||
"state/said.jsonl",
|
||||
"state/felt.jsonl",
|
||||
"state/sleep.json",
|
||||
"state/mood.json",
|
||||
"state/loops.json",
|
||||
"state/probe.jsonl",
|
||||
"memory/short-term.jsonl",
|
||||
"memory/inbox/",
|
||||
"mindmap/threads/",
|
||||
|
||||
+983
-33
File diff suppressed because it is too large
Load Diff
+242
-12
@@ -254,6 +254,27 @@ function renderTemplate(name, mapping) {
|
||||
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),
|
||||
EMOJI: str(flags.emoji),
|
||||
AVATAR: str(flags.avatar),
|
||||
// 情緒破口:`--tells "anger=不講話;把事情做完再說|羞愧=摸後頸"`。
|
||||
// 沒帶就留空——空的區塊會退回全域預設,而樣板裡的註解會告訴下一個人怎麼補。
|
||||
TELLS: renderTells(str(flags.tells)),
|
||||
CREATED: pl.nowIso(),
|
||||
};
|
||||
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("emotion-decay", () => {
|
||||
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("trim-said", () => pl.trimSaid(slug));
|
||||
await run("archive-journal", () => pl.archiveJournals(slug));
|
||||
@@ -743,7 +771,7 @@ commands.remember = ({ flags }) => {
|
||||
pl.rememberShort(slug, entry);
|
||||
const kept = pl.pruneShortTerm(slug);
|
||||
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), {
|
||||
ts: pl.nowIso(), kind: "emotion", trigger: text.slice(0, 120),
|
||||
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 lines = [`「${query}」的長期記憶命中 ${hits.length} 則:`];
|
||||
for (const meta of hits) {
|
||||
const first = (meta._body || "").split("\n")[0] || "";
|
||||
lines.push(`- ${meta._name}|${meta.type || "fact"}|顯著度 ${meta.salience ?? "?"}|${first.slice(0, 120)}`);
|
||||
const r = pl.memoryRecalled(meta);
|
||||
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);
|
||||
if (recents.length) {
|
||||
@@ -782,7 +820,8 @@ commands.recall = ({ flags }) => {
|
||||
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));
|
||||
// 模糊到只剩線索的那幾則**不算被想起來**:真的想不起來就不該重設它的衰減
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -850,12 +889,23 @@ commands.said = ({ flags, positional }) => {
|
||||
for (const issue of blockers) lines.push(`⚠ ${pl.speechLintMessage(issue)}。`);
|
||||
// hint 不算不通過(例如講到自己的過去),但要提醒去驗證
|
||||
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;
|
||||
if (clean) {
|
||||
lines.push(`✔ 沒講過,${sentences} 句,可以說。`);
|
||||
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;
|
||||
}
|
||||
die(`未知 action:${action}(可用 check/list)`);
|
||||
@@ -930,6 +980,13 @@ commands.consolidate = ({ flags }) => {
|
||||
"fact", "preference", "event", "promise", "relationship", "insight", "boundary", "canon", "diary",
|
||||
];
|
||||
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"];
|
||||
// `about` 是自由字串(原樣保留);對得上關係圖節點的才多寫一行 id,之後 recall 與
|
||||
// 「人際關係」那段才知道講的是同一個人。一個都對不上就不輸出這一行。
|
||||
@@ -949,7 +1006,16 @@ commands.consolidate = ({ flags }) => {
|
||||
...(aboutIds.length ? [`about_ids: [${aboutIds.join(", ")}]`] : []),
|
||||
`topics: [${csv(flags.topics).map(fm).filter(Boolean).join(", ")}]`,
|
||||
`salience: ${num(flags.salience, 60)}`,
|
||||
// 回想強度:連續衰減的起點。已經存在的記憶保留自己長出來的強度,
|
||||
// 不要因為重寫一次就把「被想起過很多次」的歷史抹平。
|
||||
`strength: ${num(flags.strength, Number(existing.strength ?? pl.MEMORY_STRENGTH_DEFAULT))}`,
|
||||
`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")}`,
|
||||
`first_seen: ${existing.first_seen || today}`,
|
||||
`last_seen: ${today}`,
|
||||
@@ -957,7 +1023,8 @@ commands.consolidate = ({ flags }) => {
|
||||
`source: ${fm(str(flags.source) || "short-term")}`,
|
||||
"---",
|
||||
"",
|
||||
body.trim(),
|
||||
// 主旨/細節兩層:衰減先吃細節,主旨最後才掉。沒有明寫就用第一段當主旨。
|
||||
pl.joinGistDetail(splitBody.gist, splitBody.detail),
|
||||
"",
|
||||
];
|
||||
pl.writeText(file, front.join("\n"));
|
||||
@@ -984,6 +1051,139 @@ commands.prune = ({ flags }) => {
|
||||
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 }) => {
|
||||
const session = requireSession(flags);
|
||||
const slug = hostOf(flags, session);
|
||||
@@ -1045,7 +1245,7 @@ commands.emotion = ({ flags }) => {
|
||||
deltas = rel.gain === 1
|
||||
? raw
|
||||
: 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) {
|
||||
s.last_trigger = s.last_trigger || {};
|
||||
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.appendJsonl(pl.journalPath(slug), {
|
||||
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) =>
|
||||
` ${pl.EMOTIONS[key].zh} ${key.padEnd(13)}${String(state.levels[key]).padStart(6)}(基線 ${state.baseline[key]})`;
|
||||
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 (!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 (data.host && data.host !== result.persona) {
|
||||
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
|
||||
--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 存取庫的名稱;
|
||||
不指定 --persona 就用編號當目錄名。
|
||||
load --persona <slug> --session <id> [--takeover]
|
||||
@@ -2318,15 +2537,26 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C
|
||||
→修剪 said→壓縮舊 journal→寫 sleep.json→Gitea 兩區 push+驗證。
|
||||
預設保留載入鎖(--release 才收工)。--json 只回報「睡完了沒、哪一步出錯」。
|
||||
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>
|
||||
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>]
|
||||
[--from <對象>] 這件事是誰引起的:親近度會放大/縮小 delta
|
||||
[--read "<對方說的話>"] 只讀情緒訊號與該怎麼接,不改狀態
|
||||
[--audit [--limit 20]] 最近幾輪的 delta 是不是只往一邊倒+對方的走向
|
||||
套用時會飽和(越接近端點漲越慢)並受單輪預算限制(|delta| 總和 ≤ 40)
|
||||
套用時會飽和、受單輪預算限制(|delta| 總和 ≤ 60),再經交互抑制、慣性、
|
||||
當日底色與疲勞調整——實際生效的值看 last_trigger.scaled
|
||||
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 style --session <id> --name <who> [--facet 稱呼 --value 親愛的 --except anger>=40 --since --clear]
|
||||
|
||||
Reference in New Issue
Block a user