底層本來就走得通(`pullArea` 的 restore 會把本機缺少的檔案全部補進來), 擋住的是上層的雞生蛋:`sync` 先走 `requireOwner`,而 `requireOwner` 第一件事 就是「本機沒有這個人格就 die」。本機沒有它 → load 不了它 → sync pull 被擋 → 永遠拉不回來。所以照 `import` 的模式另開一個只驗 session、不驗 host 的入口。 * `clone --code <編號>`:兩區都拉回來(Wiki 區給身分與長期記憶,檔案區給活狀態), 然後補上 `pullArea` 不管的那幾件事——驗 IDENTITY.md(`validateBundle` 明文的 人格最低要件,Gitea 這條路上原本不存在)、補寫 config.code/來歷、 `rebuildIndex()`、`renderRelations()`。拉回來不成人格就中止並清掉半成品。 * `clone`(不帶 --code):列出遠端有哪些人格、哪些本機還沒有。 整個 codebase 原本沒有任何「列出 owner 底下的存取庫」的呼叫,新增 `listRemotePersonas()`:分頁打 `GET /user/repos`(他人/組織走 `/users/<owner>/repos`), 用編號格式過濾——存取庫名稱就是人格編號,所以那份清單就是遠端的人格清單。 * 本機已有同名人格時**預設不覆蓋**;`--force` 才蓋(沿用 `import` 的兩道保護: 不得覆寫別人、不得覆寫正被其他程序載入的人格),`--persona` 可並存兩份。 * 加進 `OWNER_EXEMPT_SUBCOMMANDS`,否則已載入其他人格時會被 hook deny。 * 編號衝突:`nextCode()` 只掃本機,換機器會重複發號。新增 `nextCodeAcrossMachines()`,發號前先問遠端已經用掉哪些編號;Gitea 連不上 就退回本機答案並在輸出明講「只對過本機」。`create` 與 `code assign/next` 都改用它。 * 順手修正 `ensureRepo` 的建庫路由:`me` 取自 `resolveOwner()`,而它在有 `PERSONA_GITEA_OWNER` 時只會把那個值原封不動還回來,於是組織永遠走成 `/user/repos`(建到 token 本人底下)。改用不受該環境變數影響的 `giteaLogin()`。 測試:selftest 新增第 ㉒ 區,用 file:// 的裸倉庫當「假的 Gitea」跑完整往返 (推兩區 → 刪掉本機人格 → clone 回來 → 驗身分/長期記憶/活狀態/索引/關係圖), 並涵蓋前兩個修正(子資料夾與非 ASCII 檔名真的進了存取庫、push 覆蓋遠端的回報 與 Stop hook 只吵一次)。345 → 373 項全過。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.5 KiB
name, description
| name | description |
|---|---|
| persona-sync | 人格編號與 Gitea 儲存:指派人格編號(英文名全大寫+索引,例如 ASUNA-01)、在 Gitea 為每個人格開一個以編號命名的私有存取庫、把高頻的活狀態同步到檔案區、把低頻的身分與長期記憶同步到 Wiki 區,以及把既有人格遷移上去。當使用者說要把人格存到 Gitea、備份或同步人格、在另一台機器接續同一個人格、問人格的編號是什麼、想看人格在 Gitea 上的存取庫、或同步出現衝突時觸發。不適用於:建立新人格(persona-create,它會自動開存取庫)、離線的單檔匯出匯入(persona-transfer)、記憶固化(persona-memory)。 |
🔗 persona-sync — 人格編號與 Gitea 儲存
CLI:node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs"(一律帶 --session <PERSONA_SESSION>)
人格編號
編號 = 英文名全大寫 + 兩位索引,同名才遞增:
亞絲娜(第一個) → ASUNA-01
結衣 → YUI-01
另一個亞絲娜 → ASUNA-02
- 編號就是 Gitea 存取庫的名稱,也是新人格的本機目錄名。
- 中文/日文名字要先轉羅馬拼音:由你提議拼法(
亞絲娜 → Asuna、沈宇 → Shen Yu), 拿給使用者確認再送出——拼錯了會變成一個很難改的編號。 - 查下一個可用編號:
code next --romaji Asuna --session <id>。 發號前會先問遠端 Gitea 已經用掉哪些編號(本機看不到別台機器發過的號); Gitea 連不上時照樣發得出來,但輸出會明講「只對過本機」——那就有撞號的風險,要轉告使用者。
資料放哪裡(依更新頻率切)
| 區 | 放什麼 | 什麼時候 push |
|---|---|---|
| 檔案區(主存取庫) | 高頻活狀態:state/emotion.json、short-term.jsonl、inner.jsonl(心裡話)、said.jsonl、inbox/、mindmap/threads/、journal/ |
每輪對話後由 Stop hook 背景推送(有最小間隔) |
| Wiki 區 | 低頻設定:IDENTITY/SOUL/AGENTS/USER、人格圖示 icon.svg/icon.png、長期記憶、INDEX、心智圖、人際關係圖 |
記憶固化、改身分、改關係圖、release 時 |
Wiki 是給人讀的設定百科:Gitea 的 wiki 只有根目錄的 .md 會變成頁面,所以
memory/long-term/xxx.md 會攤平成 Memory-xxx.md(頁面顯示為「Memory xxx」),
原始路徑記在 _paths.json,拉回來時自動還原。Home 頁會自動列出所有長期記憶的連結。
本機永遠是工作副本:hook 每輪讀寫的是本機檔案,不經過網路。Gitea 掛掉、離線、沒設 token, 人格照樣能聊天,只是不同步——同步失敗永遠不阻斷對話。
人格圖示(/jsc-persona:persona-create 或 icon generate 產生)除了同步到 Wiki 區,
也會被設成存取庫頭像,Gitea 的清單上就看得到每個人格的臉。
設定
export GITEA_HOST=https://gitea.example.com # 或 PERSONA_GITEA_HOST
export GITEA_TOKEN=<個人存取權杖> # 或 PERSONA_GITEA_TOKEN
export PERSONA_GITEA_OWNER=<帳號或組織> # 選填,預設是 token 本人的帳號
export PERSONA_GITEA=off # 需要時整個關掉
export PERSONA_SYNC_MIN_SECONDS=60 # 檔案區背景 push 的最小間隔
存取庫預設私有(人格裡有使用者的個人記憶)。要公開必須是使用者明講,才加 --public。
常用流程
# 現在的同步狀態(編號、存取庫、兩區的最後 push/pull)
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sync status --session <PERSONA_SESSION>
# 手動推送(--area files|wiki|all)
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sync push --session <PERSONA_SESSION> --area all
# 從 Gitea 拉回(載入人格時會自動做一次)
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sync pull --session <PERSONA_SESSION>
把既有人格遷移上去
既有人格(還沒有編號的)要兩步:
# 1) 載入它(同步只能動本 session 載入的那個人格)
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" load --persona <舊 slug> --session <PERSONA_SESSION>
# 2) 指派編號 + 開存取庫 + 首次推送;--rename 連本機目錄名也改成編號
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" code assign \
--session <PERSONA_SESSION> --romaji "<英文名>" --rename
--rename 之後這個人格就用編號稱呼(--persona ASUNA-01)。不加 --rename 也可以,
目錄名維持原樣、只是多了一個編號與對應的存取庫。遷移前先跟使用者確認:
這會把他的個人記憶送上 Gitea(私有庫,但仍是上傳)。
在另一台機器接續同一個人格
在新機器上設好 GITEA_HOST/GITEA_TOKEN。本機已經有這個人格(只是舊了):
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" load --persona <編號> --session <PERSONA_SESSION>
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sync pull --session <PERSONA_SESSION>
本機還沒有這個人格(全新的機器):sync 的每個動作都要求「先載入那個人格」,
而本機沒有它就 load 不了它,所以走 clone——它只驗 session,不驗 host:
# 1) 遠端有哪些人格、哪些本機還沒有
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" clone --session <PERSONA_SESSION>
# 2) 挑一個拉回來(存取庫名稱就是編號)
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" clone --code ASUNA-01 --session <PERSONA_SESSION>
拉回來的是完整的人格:Wiki 區帶回身分與長期記憶,檔案區帶回情緒與短期記憶, 拉完自動重建長期記憶索引與關係圖。沒帶回來的只有鎖與租約那類執行期狀態——那本來就該由這台機器自己產生。
- 本機已經有同名人格時預設不覆蓋。真的要以遠端為準才加
--force(本機沒推上去的改動會不見), 想並存兩份就用--persona <別的目錄名>。 - 加
--load可以拉完直接載入開聊。 - 拉回來的東西沒有
IDENTITY.md(人格的最低要件)就會中止並清掉半成品,不留半個人格在本機。
衝突
兩台機器都改過同一個檔案時,load 與 sync pull 會停下來不覆蓋本機,並列出衝突的檔案。
這時候:
- 用一句話告訴使用者哪幾個檔案兩邊都動過。
- 問他要哪一邊:
- 以 Gitea 為準 →
sync pull --force(本機那份會被覆蓋) - 以 本機 為準 →
sync push(會蓋掉遠端)
- 以 Gitea 為準 →
- 不要自己選。記憶被蓋掉是不可逆的。
push 撞到別台機器時是本機贏——但會記帳
sync push(包含每輪對話後的背景推送)遇到遠端比較新時,會以本機為準覆蓋遠端,
因為本機的工作副本才是這台機器的真相來源。這條路可以走,但不會安靜地走:
- 回報「覆蓋了哪幾個檔案、上一版是哪個 commit」,並記進
state/sync.json。 sync status列得出來;下一輪的Stophook 會把它講給使用者聽一次。- 被蓋掉的內容還在 clone 的歷史裡:
git -C <人格>/.sync/files show <上一版 sha>:<檔案>。
看到這種回報就告訴使用者:另一台機器可能正在用同一個人格。要救回舊版就從上面那行取出來。
殘留的 git 鎖
push/pull 跑到一半被中斷(sub agent 被砍、視窗關掉)會在 clone 裡留下 .git/index.lock,
之後那一區的 git add 每次都失敗 → 整區同步卡住。這件事不用你處理:
這些 clone 只有 CLI 會動,所以超過 30 秒還在的鎖一定是殘骸,push 與 pull 會自己清掉再繼續。
不要叫使用者手動進 .sync/ 刪檔案,也不要自己下 rm——
sleeper 連自己的人格目錄都不能用 shell 寫,那條路本來就是死的。
真的還是失敗(例如權限問題),照原樣把錯誤訊息轉告他就好。
邊界
- 只能同步「本 session 目前載入的人格」——跨人格同步等於跨人格讀取,會被 hook 擋下。
唯一的例外是
clone:它只會新增本機還沒有的人格,讀不到任何既有人格的資料,所以不受此限。 - guest(
persona-guestsub agent)不能同步,它對人格檔案唯讀。 .sync/是兩區的 git clone 快取,不要手動編輯;砍掉它不會掉資料,下次 push 會重新 clone。- 所有面向使用者的輸出使用繁體中文(台灣用語)、UTF-8 無亂碼。