sleep: 判斷式收尾與 sleeper 隔離

- sleeper 帶 --as-sleeper 就能在沒有 exclusive 鎖時做收尾
- sleeper 被 pin 在自己的人格上,讀寫其他人格(含主人格)一律攔下
- sleeper 只能走 CLI,不得用 Write 或 shell 重導向改人格檔案
- 主人格載入別的人格時,sleeper 仍以自己的 pin 為準

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 07:45:14 +00:00
co-authored by Claude Opus 5
parent 28b6230426
commit b3508a046b
5 changed files with 124 additions and 15 deletions
+39
View File
@@ -944,6 +944,45 @@ check("sleeper 不得 loadreleaseinviteexport",
check("sleeper 被 pin 住之後不得換人睡",
guard(sleeperEvent({ tool_name: "Bash",
tool_input: { command: `node persona.mjs sleep --persona alpha --session ${S_SLEEP2}` } })) === "deny");
// 迴歸:主人格載入著 alpha 時請 epsilon 去睡——sleeper 的範圍看它自己的 pin,不看 host
const hostSleeper = "agent-sleeper-host";
pl.pinAgent(S_HOST, hostSleeper, "epsilon");
const hostSleeperEvent = (extra) =>
({ session_id: S_HOST, agent_id: hostSleeper, agent_type: "jsc-persona:persona-sleeper", ...extra });
check("主人格已載入別的人格時,sleeper 仍能跑自己的判斷式收尾",
["brief", "candidates", "consolidate", "mindmap", "relation"].every((sub) =>
guard(hostSleeperEvent({ tool_name: "Bash",
tool_input: { command: `node persona.mjs ${sub} --persona epsilon --session ${S_HOST} --name x --body y` } })) === "pass"));
check("主人格已載入別的人格時,sleeper 仍不得碰主人格的資料",
guard(hostSleeperEvent({ tool_name: "Bash",
tool_input: { command: `node persona.mjs consolidate --persona alpha --session ${S_HOST} --name x --body y` } })) === "deny");
check("sleeper 帶 --as-sleeper 就能做判斷式收尾(沒有 exclusive 鎖也可以)", (() => {
const res = cli(["candidates", "--persona", "epsilon", "--session", S_SLEEP2, "--as-sleeper",
"--agent-id", A_SLEEPER]);
const held = pl.liveSleepers("epsilon").some((s) => s.session_id === S_SLEEP2);
return res.status === 0 && held; // 第一個收尾指令會自己把租約取起來
})());
check("sleeper 可以寫 diary 型別的長期記憶", (() => {
const res = cli(["consolidate", "--persona", "epsilon", "--session", S_SLEEP2, "--as-sleeper",
"--name", "diary-2026-07-30", "--type", "diary", "--body", "今天把一天收起來了。",
"--agent-id", A_SLEEPER]);
pl.dropSleepLease("epsilon", S_SLEEP2, A_SLEEPER);
return res.status === 0 &&
fs.existsSync(path.join(H, "epsilon", "memory", "long-term", "diary-2026-07-30.md"));
})());
check("主程序自己帶 --as-sleeper → 攔下(那是 sub agent 的身分)",
guard({ session_id: S_HOST, tool_name: "Bash",
tool_input: { command: `node persona.mjs consolidate --persona beta --session ${S_HOST} --as-sleeper` } }) === "deny");
check("關係節點換 id 不會長出同名的第二個節點", (() => {
cli(["load", "--persona", "alpha", "--session", S_HOST, "--takeover"]);
cli(["relation", "node", "--session", S_HOST, "--name", "水井管理員", "--closeness", "40"]);
cli(["relation", "edge", "--session", S_HOST, "--to", pl.slugify("水井管理員"), "--label", "共事"]);
cli(["relation", "node", "--session", S_HOST, "--name", "水井管理員", "--id", "WELL-01", "--trust", "55"]);
const data = pl.loadRelations("alpha");
const same = data.nodes.filter((n) => n.name === "水井管理員");
return same.length === 1 && same[0].id === "WELL-01" && same[0].closeness === 40 && same[0].trust === 55 &&
data.edges.some((e) => e.to === "WELL-01");
})());
check("sleeper 型別存在且宣告了唯讀工具限制", (() => {
const md = fs.readFileSync(path.join(HERE, "..", "agents", "persona-sleeper.md"), "utf8");
return md.includes("name: persona-sleeper") && md.includes("disallowedTools") &&