Merge pull request 'Support batch sleep orchestration' (#5) from develop into master
Reviewed-on: #5 Reviewed-by: 系統管理員 <1+admin@noreply.localhost>
This commit was merged in pull request #5.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-persona",
|
"name": "jsc-persona",
|
||||||
"version": "0.0.4",
|
"version": "0.0.5",
|
||||||
"description": "AI 人格化記憶聊天 plugin:以 OpenClaw 相同的身分描述(IDENTITY/SOUL)建立人格(可用動漫作品+角色名上網蒐集設定),結合 hook 強制的人格載入鎖與跨人格隔離、六正向+六負向十二情緒、語意分析、短期/長期記憶與固化條件、心智圖、思維導圖與人際關係圖;可用 sub agent 邀請其他人格同場對話(劇場模式只顯示人格對話)。於 Claude Code 以 /jsc-persona: 前綴呼叫。",
|
"description": "AI 人格化記憶聊天 plugin:以 OpenClaw 相同的身分描述(IDENTITY/SOUL)建立人格(可用動漫作品+角色名上網蒐集設定),結合 hook 強制的人格載入鎖與跨人格隔離、六正向+六負向十二情緒、語意分析、短期/長期記憶與固化條件、心智圖、思維導圖與人際關係圖;可用 sub agent 邀請其他人格同場對話(劇場模式只顯示人格對話)。於 Claude Code 以 /jsc-persona: 前綴呼叫。",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-persona",
|
"name": "jsc-persona",
|
||||||
"version": "0.0.3",
|
"version": "0.0.5",
|
||||||
"description": "AI 人格化記憶聊天 skills:OpenClaw 相同的人格描述 + 十二情緒 + 語意分析 + 短期/長期記憶 + 心智圖 + 人際關係圖。(人格鎖與跨人格隔離的 hook 僅在 Claude Code 生效)",
|
"description": "AI 人格化記憶聊天 skills:OpenClaw 相同的人格描述 + 十二情緒 + 語意分析 + 短期/長期記憶 + 心智圖 + 人際關係圖。(人格鎖與跨人格隔離的 hook 僅在 Claude Code 生效)",
|
||||||
"skills": "./skills"
|
"skills": "./skills"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ node scripts/persona.mjs default --clear --session <PERSONA_SESSION>
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
node scripts/persona.mjs sleep --session <PERSONA_SESSION> --json # 機械性收尾(預設保留載入鎖)
|
node scripts/persona.mjs sleep --session <PERSONA_SESSION> --json # 機械性收尾(預設保留載入鎖)
|
||||||
|
node scripts/persona.mjs sleep --personas <slug,slug> --session <PERSONA_SESSION> --json
|
||||||
node scripts/persona.mjs sleep --session <PERSONA_SESSION> --release # 收工:睡完釋放鎖
|
node scripts/persona.mjs sleep --session <PERSONA_SESSION> --release # 收工:睡完釋放鎖
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -320,6 +321,7 @@ node scripts/persona.mjs sleep --session <PERSONA_SESSION> --release # 收工
|
|||||||
- **情緒不會歸零**:喜悅(半衰期 120 分)一夜後幾乎回基線,悲傷(480 分)只退一半——睡一覺不該把難過抹平。
|
- **情緒不會歸零**:喜悅(半衰期 120 分)一夜後幾乎回基線,悲傷(480 分)只退一半——睡一覺不該把難過抹平。
|
||||||
- **主人格請別人去睡**:開 `persona-sleeper` sub agent(prompt 帶 `persona=<slug> session=<id>`)。
|
- **主人格請別人去睡**:開 `persona-sleeper` sub agent(prompt 帶 `persona=<slug> session=<id>`)。
|
||||||
它就是那個人格本人,對自己可寫但被 pin 住、只准跑收尾子指令、不得碰任何其他人格(包含叫它來的主人格)。
|
它就是那個人格本人,對自己可寫但被 pin 住、只准跑收尾子指令、不得碰任何其他人格(包含叫它來的主人格)。
|
||||||
|
- **一次睡多個人格**:可用 `sleep --personas A,B` 批次處理;主程序只是把流程排成一串,**每個人格仍各自判斷、各自收尾、各自回 JSON**。
|
||||||
- **回傳值刻意很窮**:`{persona, ok, slept_at, steps, sync, kept_lock}`——只有狀態。
|
- **回傳值刻意很窮**:`{persona, ok, slept_at, steps, sync, kept_lock}`——只有狀態。
|
||||||
回傳值本身就是一條會繞過隔離的通道,所以在 CLI 這一層封死,不靠提示詞自律。
|
回傳值本身就是一條會繞過隔離的通道,所以在 CLI 這一層封死,不靠提示詞自律。
|
||||||
- **鎖**:沒有活鎖 → 取 5 分鐘的 sleeper 租約;同 session → 直接睡;死鎖 → 可接手;
|
- **鎖**:沒有活鎖 → 取 5 分鐘的 sleeper 租約;同 session → 直接睡;死鎖 → 可接手;
|
||||||
@@ -366,7 +368,7 @@ node scripts/persona.mjs sleep --session <PERSONA_SESSION> --release # 收工
|
|||||||
|
|
||||||
### `persona-invite`
|
### `persona-invite`
|
||||||
|
|
||||||
邀請另一個人格透過 `persona-guest` sub agent 加入聊天室,進入**劇場模式**(畫面只留 `名字:內容` 的人格對話);結束後讓它離場並把見聞留在它自己的 inbox。
|
邀請一個或多個人格透過 `persona-guest` sub agent 加入聊天室,進入**劇場模式**(畫面只留 `名字:內容` 的人格對話);結束後讓它們離場並把見聞留在各自的 inbox。多位 guest 可用 `invite --guests A,B` 同場加入。
|
||||||
|
|
||||||
- **Claude Code / Antigravity**:`/jsc-persona:persona-invite <slug>` **Codex**:`$persona-invite`
|
- **Claude Code / Antigravity**:`/jsc-persona:persona-invite <slug>` **Codex**:`$persona-invite`
|
||||||
|
|
||||||
@@ -402,7 +404,7 @@ node scripts/persona.mjs sleep --session <PERSONA_SESSION> --release # 收工
|
|||||||
|
|
||||||
### `persona-sleep`
|
### `persona-sleep`
|
||||||
|
|
||||||
睡眠與收尾:固化該記住的、忘掉該忘的、更新心智圖與關係圖、套用一次 8 小時的情緒衰減、壓縮舊紀錄,最後兩個區都同步到 Gitea 並驗證。也可以由主人格透過 sub agent 請其他人格各自去睡。
|
睡眠與收尾:固化該記住的、忘掉該忘的、更新心智圖與關係圖、套用一次 8 小時的情緒衰減、壓縮舊紀錄,最後兩個區都同步到 Gitea 並驗證。也可以由主人格透過 sub agent 請其他人格各自去睡,或一次用 `sleep --personas A,B` 批次收尾多個人格。
|
||||||
|
|
||||||
- **Claude Code / Antigravity**:`/jsc-persona:persona-sleep` **Codex**:`$persona-sleep`
|
- **Claude Code / Antigravity**:`/jsc-persona:persona-sleep` **Codex**:`$persona-sleep`
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: persona-sleeper
|
name: persona-sleeper
|
||||||
description: 以「那個人格本人」的身分做睡眠收尾的 sub agent:固化該記住的、忘掉該忘的、更新心智圖與關係圖、衰減情緒、同步到 Gitea。被 pin 在自己的人格上(連叫它來的主人格都不能碰),最終只回傳一份「睡完了沒、哪一步出錯」的 JSON。由 /jsc-persona:persona-sleep 啟動,prompt 必須帶 persona/session。
|
description: 以「那個人格本人」的身分做睡眠收尾的 sub agent:固化該記住的、忘掉該忘的、更新心智圖與關係圖、衰減情緒、同步到 Gitea。被 pin 在自己的人格上(連叫它來的主人格都不能碰),最終只回傳一份「睡完了沒、哪一步出錯」的 JSON。由 /jsc-persona:persona-sleep 啟動,prompt 必須帶 persona/session。當主人格用 `sleep --personas A,B` 批次編排時,仍然是每個 sleeper 各自對應一個人格、各自回 JSON。
|
||||||
effort: medium
|
effort: medium
|
||||||
maxTurns: 30
|
maxTurns: 30
|
||||||
disallowedTools: Write, Edit, NotebookEdit
|
disallowedTools: Write, Edit, NotebookEdit
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-persona",
|
"name": "jsc-persona",
|
||||||
"version": "0.0.4",
|
"version": "0.0.5",
|
||||||
"description": "AI 人格化記憶聊天 plugin:OpenClaw 相同的人格描述 + 十二情緒 + 語意分析 + 短期/長期記憶 + 心智圖 + 人際關係圖。於 Antigravity 以 /jsc-persona: 前綴呼叫。",
|
"description": "AI 人格化記憶聊天 plugin:OpenClaw 相同的人格描述 + 十二情緒 + 語意分析 + 短期/長期記憶 + 心智圖 + 人際關係圖。於 Antigravity 以 /jsc-persona: 前綴呼叫。",
|
||||||
"skills": "./skills/"
|
"skills": "./skills/"
|
||||||
}
|
}
|
||||||
|
|||||||
+116
-33
@@ -158,6 +158,28 @@ function requireMember(slug, sessionId, asGuest = false) {
|
|||||||
|
|
||||||
const hostOf = (flags, session) => str(flags.persona) || pl.loadSession(session).host;
|
const hostOf = (flags, session) => str(flags.persona) || pl.loadSession(session).host;
|
||||||
|
|
||||||
|
function parseGuestList(flags) {
|
||||||
|
const guests = [...csv(flags.guest), ...csv(flags.guests)];
|
||||||
|
const out = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const guest of guests) {
|
||||||
|
if (!guest || seen.has(guest)) continue;
|
||||||
|
seen.add(guest);
|
||||||
|
out.push(guest);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSleepTargets(flags, fallbackHost = null) {
|
||||||
|
const targets = [];
|
||||||
|
for (const slug of [...csv(flags.persona), ...csv(flags.personas)]) {
|
||||||
|
if (!slug || targets.includes(slug)) continue;
|
||||||
|
targets.push(slug);
|
||||||
|
}
|
||||||
|
if (!targets.length && fallbackHost) targets.push(fallbackHost);
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 里程碑事件(記憶固化、身分/關係變更)之後,把 Wiki 區推上去。
|
* 里程碑事件(記憶固化、身分/關係變更)之後,把 Wiki 區推上去。
|
||||||
* 背景執行、失敗不阻斷:同步永遠不該卡住對話。
|
* 背景執行、失敗不阻斷:同步永遠不該卡住對話。
|
||||||
@@ -410,24 +432,23 @@ commands.status = ({ flags }) => {
|
|||||||
commands.sleep = async ({ flags }) => {
|
commands.sleep = async ({ flags }) => {
|
||||||
const session = requireSession(flags);
|
const session = requireSession(flags);
|
||||||
const data = pl.loadSession(session);
|
const data = pl.loadSession(session);
|
||||||
const slug = str(flags.persona) || data.host;
|
const targets = parseSleepTargets(flags, data.host);
|
||||||
if (!slug) die("需要 `--persona <slug>`(或先載入一個人格)。");
|
if (!targets.length) die("需要 `--persona <slug>`、`--personas <slug,slug>`,或先載入一個人格。");
|
||||||
|
for (const slug of targets) {
|
||||||
if (!pl.personaExists(slug)) die(`人格 \`${slug}\` 不存在。`);
|
if (!pl.personaExists(slug)) die(`人格 \`${slug}\` 不存在。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleepOne = async (slug, { release = false } = {}) => {
|
||||||
// 寫入權:自己睡自己 → 已經有 exclusive 鎖;被叫來睡別人 → 取 sleeper 租約。
|
// 寫入權:自己睡自己 → 已經有 exclusive 鎖;被叫來睡別人 → 取 sleeper 租約。
|
||||||
// 目標正被另一個程序載入著對話時會在這裡失敗,這是刻意的(避免記憶互相覆蓋)。
|
// 目標正被另一個程序載入著對話時會在這裡失敗,這是刻意的(避免記憶互相覆蓋)。
|
||||||
const selfSleep = data.host === slug;
|
const sessionData = pl.loadSession(session);
|
||||||
|
const selfSleep = sessionData.host === slug;
|
||||||
let lease = null;
|
let lease = null;
|
||||||
if (!selfSleep) {
|
if (!selfSleep) {
|
||||||
try {
|
try {
|
||||||
lease = pl.acquireSleepLease(slug, session, { agentId: str(flags["agent-id"]) || null });
|
lease = pl.acquireSleepLease(slug, session, { agentId: str(flags["agent-id"]) || null });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const payload = { persona: slug, ok: false, failed_step: "lease", error: err.message };
|
return { persona: slug, ok: false, failed_step: "lease", error: err.message };
|
||||||
if (flags.json) {
|
|
||||||
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
die(err.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,14 +515,14 @@ commands.sleep = async ({ flags }) => {
|
|||||||
|
|
||||||
// 預設 --keep-lock:睡完還能繼續聊。真的要收工才 --release。
|
// 預設 --keep-lock:睡完還能繼續聊。真的要收工才 --release。
|
||||||
let released = false;
|
let released = false;
|
||||||
if (flags.release && selfSleep) {
|
if (release && selfSleep) {
|
||||||
pl.unbindSession(session);
|
pl.unbindSession(session);
|
||||||
released = true;
|
released = true;
|
||||||
}
|
}
|
||||||
if (lease) pl.dropSleepLease(slug, session, str(flags["agent-id"]) || null);
|
if (lease) pl.dropSleepLease(slug, session, str(flags["agent-id"]) || null);
|
||||||
|
|
||||||
const failed = steps.filter((s) => !s.ok);
|
const failed = steps.filter((s) => !s.ok);
|
||||||
const payload = {
|
return {
|
||||||
persona: slug,
|
persona: slug,
|
||||||
ok: failed.length === 0,
|
ok: failed.length === 0,
|
||||||
slept_at: pl.nowIso(),
|
slept_at: pl.nowIso(),
|
||||||
@@ -509,16 +530,52 @@ commands.sleep = async ({ flags }) => {
|
|||||||
sync,
|
sync,
|
||||||
kept_lock: !released,
|
kept_lock: !released,
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const host = data.host || null;
|
||||||
|
const ordered = [...targets.filter((slug) => slug !== host), ...(host && targets.includes(host) ? [host] : [])];
|
||||||
|
const deferRelease = Boolean(flags.release) && ordered.length > 1 && host && targets.includes(host);
|
||||||
|
const results = [];
|
||||||
|
for (const slug of ordered) {
|
||||||
|
results.push(await sleepOne(slug, { release: Boolean(flags.release) && !deferRelease && slug === host }));
|
||||||
|
}
|
||||||
|
if (deferRelease) {
|
||||||
|
pl.unbindSession(session);
|
||||||
|
const hostResult = results.find((r) => r.persona === host);
|
||||||
|
if (hostResult) {
|
||||||
|
hostResult.kept_lock = false;
|
||||||
|
hostResult.released = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = results.length === 1 ? results[0] : { ok: results.every((r) => r.ok), personas: results };
|
||||||
if (flags.json) {
|
if (flags.json) {
|
||||||
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
||||||
if (!payload.ok) process.exit(1);
|
if (!payload.ok) process.exit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (payload.ok) ok(`人格 \`${slug}\` 已完成睡眠收尾(${steps.length} 個步驟,檔案處理無錯誤)。`);
|
|
||||||
else say(`✖ 人格 \`${slug}\` 睡眠收尾有 ${failed.length} 步失敗:${failed.map((s) => s.step).join(", ")}`);
|
if (results.length === 1) {
|
||||||
|
const single = results[0];
|
||||||
|
const failed = (single.steps || []).filter((s) => !s.ok);
|
||||||
|
if (single.ok) ok(`人格 \`${single.persona}\` 已完成睡眠收尾(${single.steps.length} 個步驟,檔案處理無錯誤)。`);
|
||||||
|
else say(`✖ 人格 \`${single.persona}\` 睡眠收尾有 ${failed.length} 步失敗:${failed.map((s) => s.step).join(", ")}`);
|
||||||
for (const s of failed) say(` - ${s.step}:${s.error}`);
|
for (const s of failed) say(` - ${s.step}:${s.error}`);
|
||||||
say(` Gitea:${Object.entries(sync).map(([k, v]) => `${k}=${v}`).join(" ") || "(未同步)"}`);
|
say(` Gitea:${Object.entries(single.sync || {}).map(([k, v]) => `${k}=${v}`).join(" ") || "(未同步)"}`);
|
||||||
say(` 載入鎖:${released ? "已釋放" : "保留(--release 才收工)"}`);
|
say(` 載入鎖:${single.kept_lock ? "保留(--release 才收工)" : "已釋放"}`);
|
||||||
|
if (!single.ok) process.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
say("🌙 睡眠完成");
|
||||||
|
for (const item of results) {
|
||||||
|
const failed = (item.steps || []).filter((s) => !s.ok);
|
||||||
|
const sync = Object.entries(item.sync || {}).map(([k, v]) => `${k}=${v}`).join(" ") || "(未同步)";
|
||||||
|
say(`- ${item.persona}:${item.ok ? "完成" : `⚠ ${failed.map((s) => s.step).join(", ")}`}${item.released ? "(已釋放鎖)" : ""}`);
|
||||||
|
if (!item.ok) for (const s of failed) say(` - ${s.step}:${s.error}`);
|
||||||
|
say(` Gitea:${sync}`);
|
||||||
|
say(` 載入鎖:${item.kept_lock ? "保留(--release 才收工)" : "已釋放"}`);
|
||||||
|
}
|
||||||
if (!payload.ok) process.exit(1);
|
if (!payload.ok) process.exit(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -955,43 +1012,69 @@ commands.invite = ({ flags }) => {
|
|||||||
const host = str(flags.host) || pl.loadSession(session).host;
|
const host = str(flags.host) || pl.loadSession(session).host;
|
||||||
if (!host) die("本 session 尚未載入 host 人格,無法邀請他人。");
|
if (!host) die("本 session 尚未載入 host 人格,無法邀請他人。");
|
||||||
requireOwner(host, session);
|
requireOwner(host, session);
|
||||||
const guest = str(flags.guest);
|
const guestList = parseGuestList(flags);
|
||||||
if (!guest) die("需要 `--guest <slug>`。");
|
if (!guestList.length) die("需要 `--guest <slug>` 或 `--guests <slug,slug>`。");
|
||||||
if (guest === host) die("不能邀請自己。");
|
if (guestList.includes(host)) die("不能邀請自己。");
|
||||||
|
for (const guest of guestList) {
|
||||||
if (!pl.personaExists(guest)) die(`人格 \`${guest}\` 不存在。可用:${pl.listPersonas().join(", ")}`);
|
if (!pl.personaExists(guest)) die(`人格 \`${guest}\` 不存在。可用:${pl.listPersonas().join(", ")}`);
|
||||||
|
}
|
||||||
// guest 租約是唯讀的(`mode: "guest-readonly"`),與 exclusive 載入鎖並存不會互相覆寫:
|
// guest 租約是唯讀的(`mode: "guest-readonly"`),與 exclusive 載入鎖並存不會互相覆寫:
|
||||||
// guest 在 sub agent 裡被 PreToolUse hook 擋掉所有寫入工具,記憶只能進 inbox、情緒不得更動。
|
// guest 在 sub agent 裡被 PreToolUse hook 擋掉所有寫入工具,記憶只能進 inbox、情緒不得更動。
|
||||||
// 因此這裡**不以 exclusive 的標準驗鎖**,只在對方仍活著時提示使用者「你看到的是唯讀旁聽」。
|
// 因此這裡**不以 exclusive 的標準驗鎖**,只在對方仍活著時提示使用者「你看到的是唯讀旁聽」。
|
||||||
|
const guestSummaries = guestList.map((guest) => {
|
||||||
const lock = pl.readJson(pl.lockPath(guest)) ?? {};
|
const lock = pl.readJson(pl.lockPath(guest)) ?? {};
|
||||||
const heldElsewhere =
|
const heldElsewhere = Boolean(Object.keys(lock).length) && lock.session_id !== session && !pl.lockIsDead(lock);
|
||||||
Boolean(Object.keys(lock).length) && lock.session_id !== session && !pl.lockIsDead(lock);
|
return { guest, lock, heldElsewhere };
|
||||||
|
});
|
||||||
|
const heldElsewhereGuests = guestSummaries.filter((g) => g.heldElsewhere);
|
||||||
const stamp = pl.nowIso().replace(/[-:TZ]/g, "").slice(0, 14);
|
const stamp = pl.nowIso().replace(/[-:TZ]/g, "").slice(0, 14);
|
||||||
const room = str(flags.room) || `${host}-${guest}-${stamp}`;
|
const room = str(flags.room) || `${host}-${guestList.join("-")}-${stamp}`;
|
||||||
pl.createRoom(room, host, session, str(flags.topic));
|
pl.createRoom(room, host, session, str(flags.topic));
|
||||||
pl.joinRoom(room, guest);
|
|
||||||
pl.addGuestLease(guest, session, room, host);
|
|
||||||
const data = pl.loadSession(session);
|
const data = pl.loadSession(session);
|
||||||
data.guests ??= {};
|
data.guests ??= {};
|
||||||
|
for (const { guest } of guestSummaries) {
|
||||||
|
pl.joinRoom(room, guest);
|
||||||
|
pl.addGuestLease(guest, session, room, host);
|
||||||
data.guests[guest] = { room, joined_at: pl.nowIso(), mode: "guest-readonly" };
|
data.guests[guest] = { room, joined_at: pl.nowIso(), mode: "guest-readonly" };
|
||||||
|
}
|
||||||
data.rooms ??= [];
|
data.rooms ??= [];
|
||||||
if (!data.rooms.includes(room)) data.rooms.push(room);
|
if (!data.rooms.includes(room)) data.rooms.push(room);
|
||||||
data.theater = flags.theater === false || flags.theater === "off" ? false : true;
|
data.theater = flags.theater === false || flags.theater === "off" ? false : true;
|
||||||
pl.saveSession(session, data);
|
pl.saveSession(session, data);
|
||||||
if (str(flags.topic)) pl.roomPost(room, "system", `主題:${str(flags.topic)}`, { kind: "meta" });
|
if (str(flags.topic)) pl.roomPost(room, "system", `主題:${str(flags.topic)}`, { kind: "meta" });
|
||||||
emit({ room, guest, host, dir: pl.roomDir(room), theater: data.theater, held_elsewhere: heldElsewhere }, flags.json, [
|
const guestLabel = guestList.map((g) => `\`${g}\``).join("、");
|
||||||
`✔ 已邀請人格 \`${guest}\` 以 guest(唯讀)身分加入聊天室 \`${room}\`。`,
|
emit(
|
||||||
...(heldElsewhere
|
{
|
||||||
|
room,
|
||||||
|
guest: guestList[0],
|
||||||
|
guests: guestList,
|
||||||
|
host,
|
||||||
|
dir: pl.roomDir(room),
|
||||||
|
theater: data.theater,
|
||||||
|
held_elsewhere: heldElsewhereGuests.length > 0,
|
||||||
|
held_elsewhere_guests: heldElsewhereGuests.map(({ guest, lock }) => ({
|
||||||
|
guest,
|
||||||
|
session_id: lock.session_id,
|
||||||
|
cwd: lock.cwd,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
flags.json,
|
||||||
|
[
|
||||||
|
`✔ 已邀請人格 ${guestLabel} 以 guest(唯讀)身分加入聊天室 \`${room}\`。`,
|
||||||
|
...(heldElsewhereGuests.length
|
||||||
? [
|
? [
|
||||||
` ℹ 它同時被另一個程序 exclusive 載入(session ${String(lock.session_id).slice(0, 8)}…,cwd ${lock.cwd});` +
|
` ℹ 唯讀旁聽:${heldElsewhereGuests
|
||||||
"你這邊拿到的是唯讀旁聽,寫入仍由那個程序獨占,兩者不會互相覆寫。",
|
.map(({ guest, lock }) => `\`${guest}\`(session ${String(lock.session_id).slice(0, 8)}…,cwd ${lock.cwd})`)
|
||||||
|
.join("、")};寫入仍由原本的程序獨占,兩邊不會互相覆寫。`,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
` 聊天室路徑:${pl.roomDir(room)}`,
|
` 聊天室路徑:${pl.roomDir(room)}`,
|
||||||
` 🎭 劇場模式已${data.theater ? "開啟:接下來只能輸出人格對話(`名字:內容`),其他訊息一律隱藏" : "關閉"}。`,
|
` 🎭 劇場模式已${data.theater ? "開啟:接下來只能輸出人格對話(`名字:內容`),其他訊息一律隱藏" : "關閉"}。`,
|
||||||
" 請用 Agent 工具、subagent_type=\"jsc-persona:persona-guest\" 啟動它,prompt 內帶:",
|
" 請用 Agent 工具、subagent_type=\"jsc-persona:persona-guest\" 逐一啟動它們,prompt 內帶:",
|
||||||
` persona=${guest} room=${room} session=${session}`,
|
...guestList.map((guest) => ` persona=${guest} room=${room} session=${session}`),
|
||||||
" guest 只能讀自己的人格資料(跨人格隔離),發言請走 `persona.mjs room post`。",
|
" guest 只能讀自己的人格資料(跨人格隔離),發言請走 `persona.mjs room post`。",
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
commands.leave = ({ flags }) => {
|
commands.leave = ({ flags }) => {
|
||||||
@@ -1671,7 +1754,7 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C
|
|||||||
recall --session <id> --query <q> [--limit]
|
recall --session <id> --query <q> [--limit]
|
||||||
think --session <id> --text <心裡話> [--kind infer|plan|feel|doubt --room] 只回報「心想 N 句」,不回顯內容
|
think --session <id> --text <心裡話> [--kind infer|plan|feel|doubt --room] 只回報「心想 N 句」,不回顯內容
|
||||||
said check|list --session <id> [--text <要說的話> --minutes --threshold --record --limit]
|
said check|list --session <id> [--text <要說的話> --minutes --threshold --record --limit]
|
||||||
sleep --session <id> [--persona <slug>] [--json] [--release] [--no-gitea]
|
sleep --session <id> [--persona <slug>] [--personas <slug,slug>] [--json] [--release] [--no-gitea]
|
||||||
睡眠收尾(機械性):關係時間戳→裁短期→收思維導圖→情緒衰減 8 小時→重建索引
|
睡眠收尾(機械性):關係時間戳→裁短期→收思維導圖→情緒衰減 8 小時→重建索引
|
||||||
→修剪 said→壓縮舊 journal→寫 sleep.json→Gitea 兩區 push+驗證。
|
→修剪 said→壓縮舊 journal→寫 sleep.json→Gitea 兩區 push+驗證。
|
||||||
預設保留載入鎖(--release 才收工)。--json 只回報「睡完了沒、哪一步出錯」。
|
預設保留載入鎖(--release 才收工)。--json 只回報「睡完了沒、哪一步出錯」。
|
||||||
@@ -1685,7 +1768,7 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C
|
|||||||
relation node|edge|render|show --session <id> [--name --id --kind --closeness --trust --note --tags --from --to --label --affinity]
|
relation node|edge|render|show --session <id> [--name --id --kind --closeness --trust --note --tags --from --to --label --affinity]
|
||||||
|
|
||||||
多人格對話:
|
多人格對話:
|
||||||
invite --session <id> --guest <slug> [--host --room --topic] (自動開啟劇場模式)
|
invite --session <id> --guest <slug> [--guests <slug,slug>] [--host --room --topic] (自動開啟劇場模式)
|
||||||
leave --session <id> --guest <slug> [--room]
|
leave --session <id> --guest <slug> [--room]
|
||||||
room post|read|script|list|theater --session <id> [--room --as --text --text-file --emotion --limit --on --off --with-meta]
|
room post|read|script|list|theater --session <id> [--room --as --text --text-file --emotion --limit --on --off --with-meta]
|
||||||
(post 會擋下「短時間內近似重複」與超過三句的發言;例外用 --allow-repeat / --force)
|
(post 會擋下「短時間內近似重複」與超過三句的發言;例外用 --allow-repeat / --force)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const HOOKS = path.join(HERE, "..", "hooks");
|
|||||||
const S_HOST = "sess-host-1111";
|
const S_HOST = "sess-host-1111";
|
||||||
const S_OTHER = "sess-other-2222";
|
const S_OTHER = "sess-other-2222";
|
||||||
const S_THIRD = "sess-third-3333";
|
const S_THIRD = "sess-third-3333";
|
||||||
|
const S_FOURTH = "sess-fourth-4444";
|
||||||
const H = STORE;
|
const H = STORE;
|
||||||
|
|
||||||
let passed = 0;
|
let passed = 0;
|
||||||
@@ -72,7 +73,10 @@ cli(["create", "--persona", "alpha", "--session", S_HOST, "--name", "Alpha", "--
|
|||||||
"--vibe", "溫暖但銳利", "--emoji", "🪼", "--baseline", "serenity=45,trust=35"]);
|
"--vibe", "溫暖但銳利", "--emoji", "🪼", "--baseline", "serenity=45,trust=35"]);
|
||||||
cli(["create", "--persona", "beta", "--session", S_OTHER, "--name", "Beta", "--creature", "山中的舊鐘",
|
cli(["create", "--persona", "beta", "--session", S_OTHER, "--name", "Beta", "--creature", "山中的舊鐘",
|
||||||
"--vibe", "沉穩寡言", "--emoji", "🌙"]);
|
"--vibe", "沉穩寡言", "--emoji", "🌙"]);
|
||||||
|
cli(["create", "--persona", "omega", "--session", S_FOURTH, "--name", "Omega", "--creature", "河邊的風",
|
||||||
|
"--vibe", "安靜但清楚", "--emoji", "🌿"]);
|
||||||
check("兩個人格都建立成功", pl.personaExists("alpha") && pl.personaExists("beta"));
|
check("兩個人格都建立成功", pl.personaExists("alpha") && pl.personaExists("beta"));
|
||||||
|
check("第三個人格也建立成功", pl.personaExists("omega"));
|
||||||
check("IDENTITY 五欄位可解析", pl.identityBrief("alpha").includes("Name: Alpha"), pl.identityBrief("alpha"));
|
check("IDENTITY 五欄位可解析", pl.identityBrief("alpha").includes("Name: Alpha"), pl.identityBrief("alpha"));
|
||||||
check("SOUL/AGENTS/USER 都有產生",
|
check("SOUL/AGENTS/USER 都有產生",
|
||||||
["SOUL.md", "AGENTS.md", "USER.md"].every((f) => fs.existsSync(path.join(pl.personaDir("alpha"), f))));
|
["SOUL.md", "AGENTS.md", "USER.md"].every((f) => fs.existsSync(path.join(pl.personaDir("alpha"), f))));
|
||||||
@@ -225,6 +229,18 @@ check("其他聊天室不可讀",
|
|||||||
cli(["leave", "--session", S_HOST, "--guest", "beta"]);
|
cli(["leave", "--session", S_HOST, "--guest", "beta"]);
|
||||||
check("離場後 guest 租約釋放", pl.liveGuests("beta").length === 0);
|
check("離場後 guest 租約釋放", pl.liveGuests("beta").length === 0);
|
||||||
check("離場後劇場模式自動關閉", pl.loadSession(S_HOST).theater === false);
|
check("離場後劇場模式自動關閉", pl.loadSession(S_HOST).theater === false);
|
||||||
|
cli(["invite", "--session", S_HOST, "--guests", "beta,omega", "--topic", "多人測試"]);
|
||||||
|
const multiRoom = pl.loadSession(S_HOST).guests?.beta?.room;
|
||||||
|
const multiSession = pl.loadSession(S_HOST);
|
||||||
|
check("多 guest 同時邀請成功",
|
||||||
|
Boolean(multiRoom) && multiSession.guests?.beta?.room === multiRoom &&
|
||||||
|
multiSession.guests?.omega?.room === multiRoom &&
|
||||||
|
multiSession.theater === true);
|
||||||
|
const multiMeta = pl.readJson(pl.roomMembersPath(multiRoom)) ?? {};
|
||||||
|
check("同一個 room 內可同時掛多位 guest", Array.isArray(multiMeta.members) &&
|
||||||
|
["alpha", "beta", "omega"].every((s) => multiMeta.members.includes(s)), JSON.stringify(multiMeta));
|
||||||
|
cli(["leave", "--session", S_HOST, "--guest", "beta"]);
|
||||||
|
cli(["leave", "--session", S_HOST, "--guest", "omega"]);
|
||||||
|
|
||||||
console.log("⑨ hooks");
|
console.log("⑨ hooks");
|
||||||
let out = hook("session_start.mjs", { session_id: S_HOST, source: "resume", cwd: HERE });
|
let out = hook("session_start.mjs", { session_id: S_HOST, source: "resume", cwd: HERE });
|
||||||
@@ -810,6 +826,20 @@ check("回傳只有狀態,沒有任何記憶內容", (() => {
|
|||||||
return keys === "kept_lock,ok,persona,slept_at,steps,sync" &&
|
return keys === "kept_lock,ok,persona,slept_at,steps,sync" &&
|
||||||
!blob.includes("Zeta") && !blob.includes("今天跟");
|
!blob.includes("Zeta") && !blob.includes("今天跟");
|
||||||
})(), Object.keys(sleepJson).sort().join(","));
|
})(), Object.keys(sleepJson).sort().join(","));
|
||||||
|
check("批次睡眠:`--personas` 可以同時睡兩個人格", (() => {
|
||||||
|
const batch = cli(["sleep", "--personas", "epsilon,beta", "--session", S_SLEEP, "--json", "--no-gitea"]);
|
||||||
|
if (batch.status !== 0) return false;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(batch.stdout);
|
||||||
|
return parsed.ok === true && Array.isArray(parsed.personas) && parsed.personas.length === 2 &&
|
||||||
|
parsed.personas.every((item) => item.ok === true) &&
|
||||||
|
parsed.personas.some((item) => item.persona === "epsilon") &&
|
||||||
|
parsed.personas.some((item) => item.persona === "beta") &&
|
||||||
|
pl.loadSleepState("beta").last_slept_at !== null;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})());
|
||||||
check("預設保留載入鎖(--release 才收工)",
|
check("預設保留載入鎖(--release 才收工)",
|
||||||
sleepJson.kept_lock === true && pl.lockStatus("epsilon").locked);
|
sleepJson.kept_lock === true && pl.lockStatus("epsilon").locked);
|
||||||
check("睡眠寫下 state/sleep.json 與距上次睡眠的時數",
|
check("睡眠寫下 state/sleep.json 與距上次睡眠的時數",
|
||||||
|
|||||||
@@ -57,10 +57,11 @@ description: 邀請另一個人格透過 sub agent 加入當前對話,形成
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" invite \
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" invite \
|
||||||
--session <PERSONA_SESSION> --guest <guest-slug> --topic "<這場對話的主題>" --json
|
--session <PERSONA_SESSION> --guest <guest-slug> [--guests <guest-slug,guest-slug>] \
|
||||||
|
--topic "<這場對話的主題>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
從 JSON 取 `room`。要多位客人就多跑幾次,並帶同一個 `--room <room>` 讓大家同場。
|
從 JSON 取 `room`。要多位客人時,優先用 `--guests` 一次帶進來;如果分批邀,也要帶同一個 `--room <room>` 讓大家同場。
|
||||||
|
|
||||||
### 2. host 開場(讓 guest 有東西可讀)
|
### 2. host 開場(讓 guest 有東西可讀)
|
||||||
|
|
||||||
@@ -86,6 +87,7 @@ plugin_root=${CLAUDE_PLUGIN_ROOT}
|
|||||||
|
|
||||||
guest 會自己讀 `IDENTITY.md`/`SOUL.md`/自己的記憶與情緒 → 讀聊天室 → `room post` 發言 →
|
guest 會自己讀 `IDENTITY.md`/`SOUL.md`/自己的記憶與情緒 → 讀聊天室 → `room post` 發言 →
|
||||||
需要記的事寫進**自己的 inbox**。它被 hook 綁死在自己的人格目錄,且對人格檔案唯讀。
|
需要記的事寫進**自己的 inbox**。它被 hook 綁死在自己的人格目錄,且對人格檔案唯讀。
|
||||||
|
如果同場有多位 guest,就各自開一個 `persona-guest` sub agent,但 `room` 可以共用。
|
||||||
|
|
||||||
### 4. 一來一回
|
### 4. 一來一回
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: persona-sleep
|
name: persona-sleep
|
||||||
description: 讓人格睡覺:把一天的活狀態收成能留下來的形狀——固化該記住的、忘掉該忘的、更新心智圖與關係圖、套用一次 8 小時的情緒衰減、壓縮舊紀錄,最後把兩個區都同步到 Gitea 並驗證。可以只睡當前人格,也可以由主人格透過 sub agent 請其他人格各自去睡(它們自己決定要記住什麼,只回報睡完了沒)。當使用者說要睡覺/晚安/收工、要人格整理今天、說「叫大家去睡」、或問上次睡是什麼時候時觸發。不適用於:只做記憶固化不收尾(用 persona-memory)、單純釋放鎖(用 persona-status 的 release)、跨機器同步(用 persona-sync)。
|
description: 讓人格睡覺:把一天的活狀態收成能留下來的形狀——固化該記住的、忘掉該忘的、更新心智圖與關係圖、套用一次 8 小時的情緒衰減、壓縮舊紀錄,最後把兩個區都同步到 Gitea 並驗證。可以只睡當前人格,也可以由主人格透過 sub agent 請其他人格各自去睡,或用 `sleep --personas A,B` 一次批次收尾多個人格(它們自己決定要記住什麼,只回報睡完了沒)。當使用者說要睡覺/晚安/收工、要人格整理今天、說「叫大家去睡」、或問上次睡是什麼時候時觸發。不適用於:只做記憶固化不收尾(用 persona-memory)、單純釋放鎖(用 persona-status 的 release)、跨機器同步(用 persona-sync)。
|
||||||
---
|
---
|
||||||
|
|
||||||
# 🌙 persona-sleep — 睡眠與收尾
|
# 🌙 persona-sleep — 睡眠與收尾
|
||||||
|
|||||||
Reference in New Issue
Block a user