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:
+43
-3
@@ -581,6 +581,20 @@ export function acquireSleepLease(slug, sessionId, { agentId = null } = {}) {
|
||||
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) {
|
||||
const data = readJson(sleepersPath(slug), {}) ?? {};
|
||||
data.sleepers = (data.sleepers || []).filter(
|
||||
@@ -1311,7 +1325,22 @@ export function upsertRelationNode(slug, node) {
|
||||
const data = loadRelations(slug);
|
||||
const nodeId = node.id || slugify(node.name || "");
|
||||
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) {
|
||||
for (const [k, v] of Object.entries(node)) if (v !== null && v !== undefined) data.nodes[idx][k] = v;
|
||||
data.nodes[idx].updated_at = nowIso();
|
||||
@@ -1691,6 +1720,7 @@ export function cliInvocation(command) {
|
||||
personas: [],
|
||||
session: null,
|
||||
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-]*)/);
|
||||
if (sub) info.subcommand = sub[1];
|
||||
@@ -1804,13 +1834,17 @@ export function guardDecide(event) {
|
||||
}
|
||||
// first-touch pinning:第一個提到的人格就是它要睡的那個,之後不得換人
|
||||
if (!scope.allowed && agentId && info.personas.length) pinAgent(sessionId, agentId, info.personas[0]);
|
||||
}
|
||||
if (scope.role === "guest") {
|
||||
// sleeper 的範圍已經由它自己的 pin 決定,不能再套用 host 的判定
|
||||
// (否則主人格 load 著別人時,sleeper 連自己的收尾指令都會被擋)。
|
||||
} else if (scope.role === "guest") {
|
||||
if (!GUEST_SAFE_SUBCOMMANDS.has(sub)) {
|
||||
return deny(
|
||||
`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) {
|
||||
if (scope.allowed.length && !scope.allowed.includes(slug)) {
|
||||
return deny(`guest 只能操作被邀請的人格 ${JSON.stringify(scope.allowed)},不得碰 \`${slug}\`。`);
|
||||
@@ -1822,6 +1856,12 @@ export function guardDecide(event) {
|
||||
"`--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) {
|
||||
if (OWNER_EXEMPT_SUBCOMMANDS.has(sub)) continue;
|
||||
if (scope.host && slug !== scope.host) {
|
||||
|
||||
Reference in New Issue
Block a user