Files
persona/hooks/turn_end.mjs
T
jiantw83andClaude Opus 5 5d27051293 fix(gitea): push 撞到別台機器時是本機贏,但不能靜靜地贏
`pushArea` 遇到 non-fast-forward 時會 `git reset --hard origin/<branch>`、
把本機工作副本重新疊上去再推一次,然後回 `{ok:true, changed:true}`——
沒有任何衝突訊號。而 Stop hook 每一輪都在背景 push,所以兩台機器同時聊同一個
人格時,對方的 emotion.json/short-term.jsonl 會被靜默取代,誰都不知道。

「本機工作副本是這台機器的真相來源」這個設計選擇保留,但那條路徑現在要記帳:

* 算出「對方在分歧後改過、而我們正要蓋掉」的檔案交集,連同覆蓋前的遠端 sha
  一起回傳 `overwrote`,並寫進 state/sync.json(留最近 10 筆)。
* `sync push` 一律往 stderr 寫一行警告(--quiet 也寫,背景 push 才有痕跡),
  正常輸出與 `sync status` 都列得出「蓋掉幾個檔案、上一版是誰」。
* 背景 push 是 detached、輸出丟掉的,所以由 Stop hook 認領未回報的紀錄,
  講給使用者聽一次(劇場模式也照講——那是資料被蓋掉)。
* commit 訊息也寫進「覆蓋 N 個檔案,上一版 <sha>」,被蓋掉的內容仍可用
  `git -C <人格>/.sync/<區> show <sha>:<檔案>` 取回。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:34:29 +00:00

71 lines
3.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// Stop:情緒隨時間衰減、續租、記錄逐字,並在達到「短期→長期」條件時提醒固化。
// 劇場模式進行中不發任何提醒(那會破壞「只顯示人格對話」)。
import path from "node:path";
import { fileURLToPath } from "node:url";
import { readEvent, respond } from "./_hook.mjs";
import * as pl from "../scripts/persona-lib.mjs";
import * as gt from "../scripts/persona-gitea.mjs";
const CLI = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "scripts", "persona.mjs");
const event = readEvent();
if (!event) process.exit(0);
const sessionId = event.session_id || "unknown";
const data = pl.loadSession(sessionId);
const host = data.host;
if (!host || !pl.personaExists(host)) process.exit(0);
pl.heartbeatLock(host, sessionId);
const state = pl.decayEmotion(pl.loadEmotion(host));
pl.writeJson(pl.emotionPath(host), state);
const theater = Boolean(data.theater) && (data.rooms || []).length > 0;
const message = event.last_assistant_message || "";
if (message) {
pl.appendJsonl(pl.journalPath(host), {
ts: pl.nowIso(),
kind: "utterance",
role: "persona",
text: message.slice(0, 4000),
mood: pl.mood(state),
});
// 說出口的話存進 said.jsonl,下一輪的 <persona-context> 會提醒「這些別再說一次」。
// recordSaid 內建 10 分鐘去重,所以劇場模式裡 room post 已記過的台詞不會重覆記。
for (const line of pl.spokenLines(message, { theater })) {
pl.recordSaid(host, line, { kind: theater ? "room" : "reply" });
}
}
// 檔案區=高頻活狀態:每輪結束後背景推上 Gitea(有最小間隔、失敗不阻斷這一輪)
if (!gt.giteaProblem() && gt.personaCode(host) && gt.pushDue(host, "files")) {
gt.pushInBackground(host, "files", sessionId, CLI);
}
const out = { suppressOutput: true };
// 背景 push 撞到別台機器時是「本機覆蓋遠端」。那條路徑會把帳記在 sync.json,
// 但它是 detached 跑的、輸出丟掉,所以由這裡認領並回報一次——劇場模式也照報(那是資料被蓋掉)。
const overwrites = gt.pendingOverwrites(host);
if (overwrites.length) {
gt.markOverwritesReported(host);
const files = [...new Set(overwrites.flatMap((o) => o.files || []))];
const last = overwrites.at(-1);
out.systemMessage =
`[jsc-persona] ⚠ \`${host}\` 同步到 Gitea 時**以本機為準覆蓋了遠端** ${files.length} 個檔案` +
`${files.slice(0, 5).join(", ")}${files.length > 5 ? "…" : ""}),上一版是 ${String(last.previous).slice(0, 8)}。` +
"別台機器可能正在用同一個人格;細節見 `sync status`。";
}
if (!theater) {
const { total, candidates } = pl.promotionCandidates(host);
if (candidates.length) {
const rules = [...new Set(candidates.flatMap((c) => c.rules))].sort().join("/");
out.systemMessage =
`${out.systemMessage ? `${out.systemMessage}\n` : ""}` +
`[jsc-persona] \`${host}\` 短期記憶 ${total} 筆,${candidates.length} 組已達固化條件(${rules}` +
"→ 建議執行 /jsc-persona:persona-memory。";
}
}
respond(out);