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>
This commit is contained in:
2026-07-31 09:34:29 +00:00
co-authored by Claude Opus 5
parent b937c9ad7a
commit 5d27051293
5 changed files with 96 additions and 4 deletions
+13
View File
@@ -45,11 +45,24 @@ if (!gt.giteaProblem() && gt.personaCode(host) && gt.pushDue(host, "files")) {
}
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。";
}