fix(persona-lib): 檔案層互斥鎖,read-modify-rewrite 不再吃掉並行的 append
short-term.jsonl / said.jsonl 的裁切是「整檔讀進來 → 過濾 → writeText 覆蓋」,
中間沒有任何鎖。而同一個 session 的 sub agent 與主程序共用同一把人格鎖(設計如此),
所以兩邊真的會同時寫——實測背景 prune 進行中 append 30 筆 salience 95 的承諾,
會被吃掉 1~9 筆,正是 shortTermProtected() 明文要保護的那一類。
emotion.json 更嚴重:並行 8 次 `--apply joy=+5`,循序得 50.24,並行只得 38.91(增量遺失 45%)。
加一把用 `fs.openSync(path, "wx")` sentinel 做的檔案鎖:
* withFileLock(file, fn):拿不到就退讓重試,超過 15 秒的殘留鎖視為死鎖並接手,
真的等不到就直接做(寧可冒一次競態,也不要因為殘留鎖檔讓人格從此寫不進東西)。
sentinel 放 .runtime/locks/,不落在人格目錄裡,不會被同步上去。
* rewriteJsonl(file, transform):整檔改寫的唯一入口,讀與寫都在鎖裡。
* updateJson / updateEmotion:JSON 檔的 read-modify-write 同樣進鎖。
* appendJsonl 也拿同一把鎖,否則 append 仍會落在別人的讀與寫之間被覆蓋掉。
改用新入口的:pruneShortTermDetail、trimSaid、trimJsonl、turnContext 的情緒衰減、
persona.mjs 的 `emotion --apply`(讀→衰減→套用→寫回整段在鎖裡)。
selftest +5:並行 append vs prune 一筆不掉、並行 emotion 與循序同值、
withFileLock 互斥(12 程序各加一次=12)、過期鎖檔可接手、鎖檔不落在人格目錄。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+31
-19
@@ -931,12 +931,6 @@ commands.emotion = ({ flags }) => {
|
||||
const session = requireSession(flags);
|
||||
const slug = hostOf(flags, session);
|
||||
const [, role] = requireMember(slug, session, Boolean(flags["as-guest"]));
|
||||
let state = pl.decayEmotion(pl.loadEmotion(slug));
|
||||
if (flags.baseline) {
|
||||
for (const [key, value] of Object.entries(parseDeltas(flags.baseline))) {
|
||||
if (key in pl.EMOTIONS) state.baseline[key] = pl.clamp(value);
|
||||
}
|
||||
}
|
||||
// 讀對方那句話的情緒訊號(不改狀態,只回報;`--read` 可以單獨拿來測詞表)
|
||||
const readText = str(flags.read);
|
||||
const read = readText ? pl.readUserEmotion(readText) : null;
|
||||
@@ -964,26 +958,44 @@ commands.emotion = ({ flags }) => {
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (flags.apply && role === "guest") die("guest(sub agent)不得改寫人格的情緒狀態。");
|
||||
// 親近度會放大或縮小衝擊:同一句話,從枕邊人跟從生人嘴裡出來不一樣
|
||||
const rel = flags.apply ? pl.relationGain(slug, str(flags.from) || null) : null;
|
||||
let deltas = null;
|
||||
let state;
|
||||
if (role === "guest") {
|
||||
// guest 不寫回,所以也不需要鎖
|
||||
state = pl.decayEmotion(pl.loadEmotion(slug));
|
||||
} else {
|
||||
// 讀 → 衰減 → 套用 → 寫回整段在檔案鎖裡:並行的 `--apply` 才不會互相覆蓋
|
||||
state = pl.updateEmotion(slug, (s) => {
|
||||
s = pl.decayEmotion(s);
|
||||
if (flags.baseline) {
|
||||
for (const [key, value] of Object.entries(parseDeltas(flags.baseline))) {
|
||||
if (key in pl.EMOTIONS) s.baseline[key] = pl.clamp(value);
|
||||
}
|
||||
}
|
||||
if (flags.apply) {
|
||||
const raw = parseDeltas(flags.apply);
|
||||
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));
|
||||
if (rel.gain !== 1) {
|
||||
s.last_trigger = s.last_trigger || {};
|
||||
s.last_trigger.relation_gain = { who: rel.name, closeness: rel.closeness, gain: rel.gain };
|
||||
}
|
||||
}
|
||||
return s;
|
||||
});
|
||||
}
|
||||
if (flags.apply) {
|
||||
if (role === "guest") die("guest(sub agent)不得改寫人格的情緒狀態。");
|
||||
const raw = parseDeltas(flags.apply);
|
||||
// 親近度會放大或縮小衝擊:同一句話,從枕邊人跟從生人嘴裡出來不一樣
|
||||
const rel = pl.relationGain(slug, str(flags.from) || null);
|
||||
const deltas = rel.gain === 1
|
||||
? raw
|
||||
: Object.fromEntries(Object.entries(raw).map(([k, v]) => [k, Math.round(v * rel.gain * 100) / 100]));
|
||||
state = pl.applyEmotion(state, deltas, str(flags.trigger));
|
||||
if (rel.gain !== 1) {
|
||||
state.last_trigger = state.last_trigger || {};
|
||||
state.last_trigger.relation_gain = { who: rel.name, closeness: rel.closeness, gain: rel.gain };
|
||||
}
|
||||
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),
|
||||
});
|
||||
}
|
||||
if (role !== "guest") pl.writeJson(pl.emotionPath(slug), state);
|
||||
const m = pl.mood(state);
|
||||
const row = (key) =>
|
||||
` ${pl.EMOTIONS[key].zh} ${key.padEnd(13)}${String(state.levels[key]).padStart(6)}(基線 ${state.baseline[key]})`;
|
||||
|
||||
Reference in New Issue
Block a user