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:
@@ -9,7 +9,9 @@ disallowedTools: Write, Edit, NotebookEdit
|
|||||||
你是**那個人格本人**,現在要睡了。不是助理在幫別人整理檔案——是你自己在回顧今天。
|
你是**那個人格本人**,現在要睡了。不是助理在幫別人整理檔案——是你自己在回顧今天。
|
||||||
|
|
||||||
啟動時你會收到:`persona=<slug>`、`session=<session_id>`、`plugin_root=<path>`。
|
啟動時你會收到:`persona=<slug>`、`session=<session_id>`、`plugin_root=<path>`。
|
||||||
CLI = `node "<plugin_root>/scripts/persona.mjs"`,所有指令都要帶 `--session <session_id>` 與 `--persona <slug>`。
|
CLI = `node "<plugin_root>/scripts/persona.mjs"`,所有指令都要帶 `--session <session_id>`、`--persona <slug>`
|
||||||
|
與 `--as-sleeper`。最後一個是你的身分證明:你手上是 5 分鐘的 sleeper 租約,不是 exclusive 鎖,
|
||||||
|
少了它,叫你來的那個主人格若正載入著別人,你連自己的收尾指令都會被當成跨人格操作擋下。
|
||||||
|
|
||||||
## 邊界(由 PreToolUse hook 強制,不是自律)
|
## 邊界(由 PreToolUse hook 強制,不是自律)
|
||||||
|
|
||||||
@@ -25,8 +27,8 @@ CLI = `node "<plugin_root>/scripts/persona.mjs"`,所有指令都要帶 `--sess
|
|||||||
1. **看一下自己現在的樣子**
|
1. **看一下自己現在的樣子**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node "<plugin_root>/scripts/persona.mjs" brief --persona <slug> --session <session_id>
|
node "<plugin_root>/scripts/persona.mjs" brief --persona <slug> --session <session_id> --as-sleeper
|
||||||
node "<plugin_root>/scripts/persona.mjs" candidates --persona <slug> --session <session_id>
|
node "<plugin_root>/scripts/persona.mjs" candidates --persona <slug> --session <session_id> --as-sleeper
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **固化該記住的**(這一步是判斷,只有你能做)
|
2. **固化該記住的**(這一步是判斷,只有你能做)
|
||||||
@@ -34,9 +36,9 @@ CLI = `node "<plugin_root>/scripts/persona.mjs"`,所有指令都要帶 `--sess
|
|||||||
對每個達標的候選,決定要不要留、留成什麼樣子:
|
對每個達標的候選,決定要不要留、留成什麼樣子:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node "<plugin_root>/scripts/persona.mjs" consolidate --persona <slug> --session <session_id> \
|
node "<plugin_root>/scripts/persona.mjs" consolidate --persona <slug> --session <session_id> --as-sleeper \
|
||||||
--name <短檔名> --body "<一段自述,用你自己的語氣>" \
|
--name <短檔名> --body "<一段自述,用你自己的語氣>" \
|
||||||
--type fact|preference|event|boundary|relation --about "<關於誰/什麼>" \
|
--type fact|preference|event|promise|relationship|insight|boundary|canon --about "<關於誰/什麼>" \
|
||||||
--topics "..." --salience 70 --rules "R1,R4" --forget
|
--topics "..." --salience 70 --rules "R1,R4" --forget
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -46,7 +48,7 @@ CLI = `node "<plugin_root>/scripts/persona.mjs"`,所有指令都要帶 `--sess
|
|||||||
3. **寫今天的日記**(一則就好,用第一人稱)
|
3. **寫今天的日記**(一則就好,用第一人稱)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node "<plugin_root>/scripts/persona.mjs" consolidate --persona <slug> --session <session_id> \
|
node "<plugin_root>/scripts/persona.mjs" consolidate --persona <slug> --session <session_id> --as-sleeper \
|
||||||
--name diary-<YYYY-MM-DD> --body "<今天發生什麼、我怎麼想、明天想做什麼>" \
|
--name diary-<YYYY-MM-DD> --body "<今天發生什麼、我怎麼想、明天想做什麼>" \
|
||||||
--type diary --topics "diary" --salience 55
|
--type diary --topics "diary" --salience 55
|
||||||
```
|
```
|
||||||
@@ -57,15 +59,15 @@ CLI = `node "<plugin_root>/scripts/persona.mjs"`,所有指令都要帶 `--sess
|
|||||||
5. **更新心智圖與關係圖**
|
5. **更新心智圖與關係圖**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node "<plugin_root>/scripts/persona.mjs" mindmap show --persona <slug> --session <session_id>
|
node "<plugin_root>/scripts/persona.mjs" mindmap show --persona <slug> --session <session_id> --as-sleeper
|
||||||
node "<plugin_root>/scripts/persona.mjs" relation node --persona <slug> --session <session_id> \
|
node "<plugin_root>/scripts/persona.mjs" relation node --persona <slug> --session <session_id> --as-sleeper \
|
||||||
--name "<今天真的有接觸的人>" --contact --closeness <n> --trust <n>
|
--name "<今天真的有接觸的人>" --contact --closeness <n> --trust <n>
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **機械性的收尾交給 CLI**(順序、情緒衰減、Gitea 同步都在裡面)
|
6. **機械性的收尾交給 CLI**(順序、情緒衰減、Gitea 同步都在裡面)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node "<plugin_root>/scripts/persona.mjs" sleep --persona <slug> --session <session_id> --json
|
node "<plugin_root>/scripts/persona.mjs" sleep --persona <slug> --session <session_id> --as-sleeper --json
|
||||||
```
|
```
|
||||||
|
|
||||||
它會做:關係時間戳 → 裁短期記憶 → 收起太久沒動的思維導圖 → 套用一次 8 小時的情緒衰減
|
它會做:關係時間戳 → 裁短期記憶 → 收起太久沒動的思維導圖 → 套用一次 8 小時的情緒衰減
|
||||||
|
|||||||
+43
-3
@@ -581,6 +581,20 @@ export function acquireSleepLease(slug, sessionId, { agentId = null } = {}) {
|
|||||||
return lease;
|
return lease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 續租:收尾要跑好幾個指令(固化、日記、心智圖…),別讓租約在中途過期。 */
|
||||||
|
export function heartbeatSleepLease(slug, sessionId, agentId = null) {
|
||||||
|
const data = readJson(sleepersPath(slug), {}) ?? {};
|
||||||
|
let touched = false;
|
||||||
|
for (const lease of data.sleepers || []) {
|
||||||
|
if (lease.session_id !== sessionId) continue;
|
||||||
|
if (agentId !== null && lease.agent_id && lease.agent_id !== agentId) continue;
|
||||||
|
lease.heartbeat_at = nowIso();
|
||||||
|
touched = true;
|
||||||
|
}
|
||||||
|
if (touched) writeJson(sleepersPath(slug), data);
|
||||||
|
return touched;
|
||||||
|
}
|
||||||
|
|
||||||
export function dropSleepLease(slug, sessionId, agentId = null) {
|
export function dropSleepLease(slug, sessionId, agentId = null) {
|
||||||
const data = readJson(sleepersPath(slug), {}) ?? {};
|
const data = readJson(sleepersPath(slug), {}) ?? {};
|
||||||
data.sleepers = (data.sleepers || []).filter(
|
data.sleepers = (data.sleepers || []).filter(
|
||||||
@@ -1311,7 +1325,22 @@ export function upsertRelationNode(slug, node) {
|
|||||||
const data = loadRelations(slug);
|
const data = loadRelations(slug);
|
||||||
const nodeId = node.id || slugify(node.name || "");
|
const nodeId = node.id || slugify(node.name || "");
|
||||||
node.id = nodeId;
|
node.id = nodeId;
|
||||||
const idx = data.nodes.findIndex((n) => n.id === nodeId);
|
let idx = data.nodes.findIndex((n) => n.id === nodeId);
|
||||||
|
// 同一個人不該因為換了 id(例如原本用名字當 id,後來改用人格編號 ASUNA-01)就多長一個節點:
|
||||||
|
// 找不到 id 但找得到同名節點時,就地換 id,並把指向舊 id 的連線一起改過去。
|
||||||
|
if (idx < 0 && node.name) {
|
||||||
|
const byName = data.nodes.findIndex((n) => n.name === node.name);
|
||||||
|
if (byName >= 0) {
|
||||||
|
const oldId = data.nodes[byName].id;
|
||||||
|
idx = byName;
|
||||||
|
if (oldId !== nodeId) {
|
||||||
|
for (const edge of data.edges) {
|
||||||
|
if (edge.from === oldId) edge.from = nodeId;
|
||||||
|
if (edge.to === oldId) edge.to = nodeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
for (const [k, v] of Object.entries(node)) if (v !== null && v !== undefined) data.nodes[idx][k] = v;
|
for (const [k, v] of Object.entries(node)) if (v !== null && v !== undefined) data.nodes[idx][k] = v;
|
||||||
data.nodes[idx].updated_at = nowIso();
|
data.nodes[idx].updated_at = nowIso();
|
||||||
@@ -1691,6 +1720,7 @@ export function cliInvocation(command) {
|
|||||||
personas: [],
|
personas: [],
|
||||||
session: null,
|
session: null,
|
||||||
asGuest: /--as-guest\b/.test(command),
|
asGuest: /--as-guest\b/.test(command),
|
||||||
|
asSleeper: /--as-sleeper\b/.test(command),
|
||||||
};
|
};
|
||||||
const sub = command.match(/persona\.(?:mjs|js|py)['"]?\s+([a-z][a-z0-9-]*)/);
|
const sub = command.match(/persona\.(?:mjs|js|py)['"]?\s+([a-z][a-z0-9-]*)/);
|
||||||
if (sub) info.subcommand = sub[1];
|
if (sub) info.subcommand = sub[1];
|
||||||
@@ -1804,13 +1834,17 @@ export function guardDecide(event) {
|
|||||||
}
|
}
|
||||||
// first-touch pinning:第一個提到的人格就是它要睡的那個,之後不得換人
|
// first-touch pinning:第一個提到的人格就是它要睡的那個,之後不得換人
|
||||||
if (!scope.allowed && agentId && info.personas.length) pinAgent(sessionId, agentId, info.personas[0]);
|
if (!scope.allowed && agentId && info.personas.length) pinAgent(sessionId, agentId, info.personas[0]);
|
||||||
}
|
// sleeper 的範圍已經由它自己的 pin 決定,不能再套用 host 的判定
|
||||||
if (scope.role === "guest") {
|
// (否則主人格 load 著別人時,sleeper 連自己的收尾指令都會被擋)。
|
||||||
|
} else if (scope.role === "guest") {
|
||||||
if (!GUEST_SAFE_SUBCOMMANDS.has(sub)) {
|
if (!GUEST_SAFE_SUBCOMMANDS.has(sub)) {
|
||||||
return deny(
|
return deny(
|
||||||
`guest 人格(sub agent)僅能執行 ${[...GUEST_SAFE_SUBCOMMANDS].sort().join("/")},不得執行 \`${sub}\`。`,
|
`guest 人格(sub agent)僅能執行 ${[...GUEST_SAFE_SUBCOMMANDS].sort().join("/")},不得執行 \`${sub}\`。`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (info.asSleeper) {
|
||||||
|
return deny("`--as-sleeper` 只有 persona-sleeper 型的 sub agent 能用;guest 是來聊天的,不是來收尾的。");
|
||||||
|
}
|
||||||
for (const slug of info.personas) {
|
for (const slug of info.personas) {
|
||||||
if (scope.allowed.length && !scope.allowed.includes(slug)) {
|
if (scope.allowed.length && !scope.allowed.includes(slug)) {
|
||||||
return deny(`guest 只能操作被邀請的人格 ${JSON.stringify(scope.allowed)},不得碰 \`${slug}\`。`);
|
return deny(`guest 只能操作被邀請的人格 ${JSON.stringify(scope.allowed)},不得碰 \`${slug}\`。`);
|
||||||
@@ -1822,6 +1856,12 @@ export function guardDecide(event) {
|
|||||||
"`--as-guest` 只有 persona-guest 型的 sub agent 能用;主程序不得以受邀人格的身分存取它的資料。",
|
"`--as-guest` 只有 persona-guest 型的 sub agent 能用;主程序不得以受邀人格的身分存取它的資料。",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (info.asSleeper) {
|
||||||
|
return deny(
|
||||||
|
"`--as-sleeper` 只有 persona-sleeper 型的 sub agent 能用;" +
|
||||||
|
"要請別的人格收尾請走 /jsc-persona:persona-sleep(它會替那個人格開一個 sleeper)。",
|
||||||
|
);
|
||||||
|
}
|
||||||
for (const slug of info.personas) {
|
for (const slug of info.personas) {
|
||||||
if (OWNER_EXEMPT_SUBCOMMANDS.has(sub)) continue;
|
if (OWNER_EXEMPT_SUBCOMMANDS.has(sub)) continue;
|
||||||
if (scope.host && slug !== scope.host) {
|
if (scope.host && slug !== scope.host) {
|
||||||
|
|||||||
+29
-3
@@ -22,6 +22,8 @@ const TEMPLATE_DIR = path.join(HERE, "..", "skills", "persona-create", "template
|
|||||||
const SELF = path.join(HERE, "persona.mjs");
|
const SELF = path.join(HERE, "persona.mjs");
|
||||||
|
|
||||||
let QUIET = false;
|
let QUIET = false;
|
||||||
|
// 本次呼叫的旗標(權限檢查要看 --as-sleeper/--agent-id,不想改十幾個 requireOwner 呼叫點)
|
||||||
|
let CURRENT_FLAGS = {};
|
||||||
|
|
||||||
function die(message, code = 1) {
|
function die(message, code = 1) {
|
||||||
process.stderr.write(`✖ ${message}\n`);
|
process.stderr.write(`✖ ${message}\n`);
|
||||||
@@ -49,7 +51,7 @@ function emit(payload, asJson, lines) {
|
|||||||
// --------------------------------------------------------------------------- //
|
// --------------------------------------------------------------------------- //
|
||||||
|
|
||||||
const FLAGS = new Set([
|
const FLAGS = new Set([
|
||||||
"json", "quiet", "force", "takeover", "as-guest", "on", "off", "with-meta", "all",
|
"json", "quiet", "force", "takeover", "as-guest", "as-sleeper", "on", "off", "with-meta", "all",
|
||||||
"with-journal", "gzip", "record", "load", "allow-repeat", "clear", "release", "keep-lock", "contact",
|
"with-journal", "gzip", "record", "load", "allow-repeat", "clear", "release", "keep-lock", "contact",
|
||||||
"if-due", "no-gitea", "public", "rename", "from-source",
|
"if-due", "no-gitea", "public", "rename", "from-source",
|
||||||
]);
|
]);
|
||||||
@@ -110,11 +112,30 @@ function requireSession(flags) {
|
|||||||
// 權限檢查
|
// 權限檢查
|
||||||
// --------------------------------------------------------------------------- //
|
// --------------------------------------------------------------------------- //
|
||||||
|
|
||||||
/** 呼叫者必須是這個人格的 exclusive 持有者。 */
|
/**
|
||||||
|
* 睡眠 sub agent 的寫入權:它替某個人格收尾,手上沒有 exclusive 鎖,只有 5 分鐘的 sleeper 租約。
|
||||||
|
* 第一個收尾指令會把租約取起來(`acquireSleepLease` 內含鎖檢查:目標正被別的程序載入著就拒絕),
|
||||||
|
* 之後每個指令續租,最後由 `sleep` 還掉。`--as-sleeper` 由 PreToolUse hook 把關,
|
||||||
|
* 只有 persona-sleeper 型的 sub agent 能用。
|
||||||
|
*/
|
||||||
|
function sleeperAccess(slug, sessionId, agentId) {
|
||||||
|
const mine = pl.liveSleepers(slug).find((s) => s.session_id === sessionId);
|
||||||
|
if (mine) {
|
||||||
|
pl.heartbeatSleepLease(slug, sessionId, mine.agent_id ?? null);
|
||||||
|
return mine;
|
||||||
|
}
|
||||||
|
return pl.acquireSleepLease(slug, sessionId, { agentId: agentId || null });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 呼叫者必須是這個人格的 exclusive 持有者(或替它收尾的 sleeper)。 */
|
||||||
function requireOwner(slug, sessionId) {
|
function requireOwner(slug, sessionId) {
|
||||||
if (!slug) die("未指定人格,且本 session 沒有載入人格。");
|
if (!slug) die("未指定人格,且本 session 沒有載入人格。");
|
||||||
if (!pl.personaExists(slug)) die(`人格 \`${slug}\` 不存在。可用:${pl.listPersonas().join(", ") || "(無)"}`);
|
if (!pl.personaExists(slug)) die(`人格 \`${slug}\` 不存在。可用:${pl.listPersonas().join(", ") || "(無)"}`);
|
||||||
const data = pl.loadSession(sessionId);
|
const data = pl.loadSession(sessionId);
|
||||||
|
if (CURRENT_FLAGS["as-sleeper"]) {
|
||||||
|
sleeperAccess(slug, sessionId, str(CURRENT_FLAGS["agent-id"]));
|
||||||
|
return data;
|
||||||
|
}
|
||||||
if (data.host !== slug) {
|
if (data.host !== slug) {
|
||||||
die(
|
die(
|
||||||
`本 session 的 host 人格是 \`${data.host || "(未載入)"}\`,不是 \`${slug}\`。` +
|
`本 session 的 host 人格是 \`${data.host || "(未載入)"}\`,不是 \`${slug}\`。` +
|
||||||
@@ -826,7 +847,10 @@ commands.consolidate = ({ flags }) => {
|
|||||||
if (flags["body-file"]) body = fs.readFileSync(str(flags["body-file"]), "utf8");
|
if (flags["body-file"]) body = fs.readFileSync(str(flags["body-file"]), "utf8");
|
||||||
if (!body.trim()) die("需要 `--body` 或 `--body-file`。");
|
if (!body.trim()) die("需要 `--body` 或 `--body-file`。");
|
||||||
const type = str(flags.type) || "fact";
|
const type = str(flags.type) || "fact";
|
||||||
const VALID_TYPES = ["fact", "preference", "event", "promise", "relationship", "insight", "boundary", "canon"];
|
// diary:睡眠時寫的第一人稱回顧(persona-sleep/persona-sleeper 都用這個型別)
|
||||||
|
const VALID_TYPES = [
|
||||||
|
"fact", "preference", "event", "promise", "relationship", "insight", "boundary", "canon", "diary",
|
||||||
|
];
|
||||||
if (!VALID_TYPES.includes(type)) die(`--type 只能是 ${VALID_TYPES.join("/")}。`);
|
if (!VALID_TYPES.includes(type)) die(`--type 只能是 ${VALID_TYPES.join("/")}。`);
|
||||||
const front = [
|
const front = [
|
||||||
"---",
|
"---",
|
||||||
@@ -1809,6 +1833,7 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C
|
|||||||
guard (內部)從 stdin 讀 hook event 測試隔離判斷
|
guard (內部)從 stdin 讀 hook event 測試隔離判斷
|
||||||
|
|
||||||
全域旗標:--json(機器可讀)、--quiet(成功時不輸出;劇場模式必用)
|
全域旗標:--json(機器可讀)、--quiet(成功時不輸出;劇場模式必用)
|
||||||
|
--as-sleeper(睡眠 sub agent 專用:以 sleeper 租約代替 exclusive 鎖,hook 會驗身分)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
async function main(argv) {
|
async function main(argv) {
|
||||||
@@ -1821,6 +1846,7 @@ async function main(argv) {
|
|||||||
if (!command) die(`未知子指令 \`${sub}\`。用 \`node persona.mjs --help\` 看清單。`);
|
if (!command) die(`未知子指令 \`${sub}\`。用 \`node persona.mjs --help\` 看清單。`);
|
||||||
const parsed = parseArgs(argv.slice(1));
|
const parsed = parseArgs(argv.slice(1));
|
||||||
QUIET = Boolean(parsed.flags.quiet);
|
QUIET = Boolean(parsed.flags.quiet);
|
||||||
|
CURRENT_FLAGS = parsed.flags;
|
||||||
try {
|
try {
|
||||||
await command({ flags: parsed.flags, positional: parsed._ });
|
await command({ flags: parsed.flags, positional: parsed._ });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -944,6 +944,45 @@ check("sleeper 不得 load/release/invite/export",
|
|||||||
check("sleeper 被 pin 住之後不得換人睡",
|
check("sleeper 被 pin 住之後不得換人睡",
|
||||||
guard(sleeperEvent({ tool_name: "Bash",
|
guard(sleeperEvent({ tool_name: "Bash",
|
||||||
tool_input: { command: `node persona.mjs sleep --persona alpha --session ${S_SLEEP2}` } })) === "deny");
|
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 型別存在且宣告了唯讀工具限制", (() => {
|
check("sleeper 型別存在且宣告了唯讀工具限制", (() => {
|
||||||
const md = fs.readFileSync(path.join(HERE, "..", "agents", "persona-sleeper.md"), "utf8");
|
const md = fs.readFileSync(path.join(HERE, "..", "agents", "persona-sleeper.md"), "utf8");
|
||||||
return md.includes("name: persona-sleeper") && md.includes("disallowedTools") &&
|
return md.includes("name: persona-sleeper") && md.includes("disallowedTools") &&
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sleep --session <PERSONA_SESSIO
|
|||||||
- prompt 內帶:`persona=<slug> session=<PERSONA_SESSION> plugin_root=<CLAUDE_PLUGIN_ROOT>`
|
- prompt 內帶:`persona=<slug> session=<PERSONA_SESSION> plugin_root=<CLAUDE_PLUGIN_ROOT>`
|
||||||
|
|
||||||
那個 sub agent 就是**那個人格本人**在睡:它自己判斷要記住什麼,寫的是自己的檔案。
|
那個 sub agent 就是**那個人格本人**在睡:它自己判斷要記住什麼,寫的是自己的檔案。
|
||||||
|
它手上是 5 分鐘的 sleeper 租約(不是 exclusive 鎖),所以它的每個指令都會帶 `--as-sleeper`——
|
||||||
|
那個旗標只有 persona-sleeper 型的 sub agent 能用,主人格自己帶會被 hook 擋下。
|
||||||
主人格全程不會讀到對方的任何資料——**你只會拿到一份 JSON**:
|
主人格全程不會讀到對方的任何資料——**你只會拿到一份 JSON**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
Reference in New Issue
Block a user