`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>
71 lines
3.1 KiB
JavaScript
71 lines
3.1 KiB
JavaScript
#!/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);
|