feat: 改寫為 Node.js,新增動漫角色建人格、劇場模式與記憶固化條件
腳本全面從 Python 改寫為 Node.js(ESM,只用內建模組,無 npm 依賴): scripts/persona-lib.mjs(核心)、scripts/persona.mjs(CLI)、hooks/*.mjs(六個 hook)、scripts/selftest.mjs(68 項自我測試,全綠)。 新增: - persona-anime skill:用「動漫作品+角色名」建立人格,先上網蒐集至少三個獨立 來源的公開設定,映射成 OpenClaw 的 IDENTITY 五欄位與 SOUL 四段落,再固化成 canon 基礎記憶(每則帶來源 URL)+原作人際關係圖+依角色型別的情緒基線; 必寫 roleplay-frame 界線記憶(非官方、非本人)。 - 劇場模式:invite 後只顯示人格對話(`名字:內容`)。UserPromptSubmit hook 每輪 注入強制規則、Stop hook 完全靜音,CLI 新增 --quiet 與 room script(乾淨對話稿)。 leave 後沒客人自動關閉,也可用 room theater --on/--off 手動切換。 - 短期→長期記憶的成文轉入條件 R1–R6(promotionCandidates)與 candidates 子指令, hook 在達標時提醒固化;長期記憶新增 canon 型別與 rules 欄位。 - 人格改為「由使用者呼叫才載入」:SessionStart hook 只列出可用人格,不自動附身。 其他:版本號改回 0.0.1;README/AGENTS.md 同步更新。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
// _hook.mjs — hook 共用小工具:讀 stdin 的 JSON event、輸出 hook 回應。
|
||||
import fs from "node:fs";
|
||||
|
||||
export function readEvent() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function respond(payload) {
|
||||
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
// PreToolUse guard:人格鎖驗證 + 跨人格資料隔離(唯一的強制執行點)。
|
||||
//
|
||||
// 擋下的情形:
|
||||
// * 讀寫非「本 session 當前人格」的人格目錄(含 Read/Write/Edit/Glob/Grep/Bash)
|
||||
// * guest(persona-guest sub agent)寫入任何人格檔案,或換讀別的人格
|
||||
// * CLI 帶假的 --session(冒用其他程序身分)/主程序冒用 --as-guest
|
||||
// * 目標人格的鎖屬於其他還活著的程序
|
||||
|
||||
import { readEvent, respond } from "./_hook.mjs";
|
||||
import * as pl from "../scripts/persona-lib.mjs";
|
||||
|
||||
const event = readEvent();
|
||||
if (!event) process.exit(0);
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = pl.guardDecide(event);
|
||||
} catch (err) {
|
||||
// guard 自己壞掉不該擋住整個 session
|
||||
process.stderr.write(`persona guard 內部錯誤:${err.message}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (result.decision !== "deny") process.exit(0);
|
||||
|
||||
respond({
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "PreToolUse",
|
||||
permissionDecision: "deny",
|
||||
permissionDecisionReason: `[jsc-persona 隔離] ${result.reason}`,
|
||||
},
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PreToolUse guard:人格鎖驗證 + 跨人格資料隔離(唯一的強制執行點)。
|
||||
|
||||
擋下的情形:
|
||||
* 讀寫非「本 session 當前人格」的人格目錄(含 Read/Write/Edit/Glob/Grep/Bash)
|
||||
* guest(persona-guest sub agent)寫入任何人格檔案,或換讀別的人格
|
||||
* CLI 帶假的 --session(冒用其他程序身分)
|
||||
* 目標人格的鎖屬於其他還活著的程序
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
try:
|
||||
decision, reason = pl.guard_decide(event)
|
||||
except Exception as exc: # guard 自己壞掉不該擋住整個 session
|
||||
print(f"persona guard 內部錯誤:{exc}", file=sys.stderr)
|
||||
return 0
|
||||
if decision != "deny":
|
||||
return 0
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": f"[jsc-persona 隔離] {reason}",
|
||||
}
|
||||
}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+6
-6
@@ -5,7 +5,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/session_start.py\"",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session_start.mjs\"",
|
||||
"timeout": 20,
|
||||
"statusMessage": "載入人格執行環境…"
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/prompt_submit.py\"",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/prompt_submit.mjs\"",
|
||||
"timeout": 20,
|
||||
"statusMessage": "回想記憶與情緒…"
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/guard.py\"",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/guard.mjs\"",
|
||||
"timeout": 15,
|
||||
"statusMessage": "檢查人格隔離…"
|
||||
}
|
||||
@@ -42,7 +42,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/turn_end.py\"",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/turn_end.mjs\"",
|
||||
"timeout": 20
|
||||
}
|
||||
]
|
||||
@@ -53,7 +53,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/subagent_stop.py\"",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/subagent_stop.mjs\"",
|
||||
"timeout": 15
|
||||
}
|
||||
]
|
||||
@@ -64,7 +64,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/session_end.py\"",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session_end.mjs\"",
|
||||
"timeout": 20
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
// UserPromptSubmit:每輪注入「人格 + 情緒 + 記憶 + 關係」上下文,並記錄原始逐字。
|
||||
//
|
||||
// 短期記憶(memory/short-term.jsonl)由 skill 做完語意分析後才寫入(有主題/實體/
|
||||
// 顯著度/情緒變化);這裡只寫 journal 原始逐字,避免同一句話被記兩次。
|
||||
// 劇場模式(多人格對話)進行中時,額外強制「只輸出人格對話」。
|
||||
|
||||
import { readEvent, respond } from "./_hook.mjs";
|
||||
import * as pl from "../scripts/persona-lib.mjs";
|
||||
|
||||
const event = readEvent();
|
||||
if (!event) process.exit(0);
|
||||
|
||||
const sessionId = event.session_id || "unknown";
|
||||
const prompt = event.prompt || "";
|
||||
const data = pl.loadSession(sessionId);
|
||||
const host = data.host;
|
||||
if (!host || !pl.personaExists(host)) process.exit(0);
|
||||
|
||||
pl.heartbeatLock(host, sessionId);
|
||||
for (const [guest, info] of Object.entries(data.guests || {})) {
|
||||
if (pl.personaExists(guest)) pl.addGuestLease(guest, sessionId, info.room || "", host);
|
||||
}
|
||||
|
||||
pl.appendJsonl(pl.journalPath(host), {
|
||||
ts: pl.nowIso(),
|
||||
kind: "utterance",
|
||||
role: "user",
|
||||
text: prompt.slice(0, 4000),
|
||||
});
|
||||
|
||||
const theater = Boolean(data.theater) && (data.rooms || []).length > 0;
|
||||
const hint = theater
|
||||
? "\n🎭 劇場模式:本輪輸出**只能**是人格對話(`名字:內容`,一行一句)。" +
|
||||
"不要輸出指令、指令結果、狀態、分析、旁白或摘要;CLI 一律加 `--quiet` 並把 stdout 丟到 /dev/null。" +
|
||||
"\n流程請照 /jsc-persona:persona-invite。"
|
||||
: "\n回覆前請:①做語意分析(意圖/主題/實體/情感)②依十二情緒更新狀態" +
|
||||
" ③以人格語氣回覆 ④用 `persona.mjs remember` 寫回短期記憶。" +
|
||||
"\n情緒與記憶指令請參考 /jsc-persona:persona-chat。";
|
||||
|
||||
respond({
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "UserPromptSubmit",
|
||||
additionalContext: pl.turnContext(host, sessionId, prompt) + hint,
|
||||
},
|
||||
suppressOutput: true,
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""UserPromptSubmit:每輪注入「人格 + 情緒 + 記憶 + 關係」上下文,並記錄原始逐字。
|
||||
|
||||
短期記憶(memory/short-term.jsonl)由 skill 做完語意分析後才寫入(有主題/實體/
|
||||
顯著度/情緒變化);這裡只寫 journal 原始逐字,避免同一句話被記兩次。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
prompt = event.get("prompt") or ""
|
||||
data = pl.load_session(session_id)
|
||||
host = data.get("host")
|
||||
if not host or not pl.persona_exists(host):
|
||||
return 0
|
||||
|
||||
pl.heartbeat_lock(host, session_id)
|
||||
for guest, info in (data.get("guests") or {}).items():
|
||||
if pl.persona_exists(guest):
|
||||
pl.add_guest_lease(guest, session_id, info.get("room", ""), host)
|
||||
|
||||
pl.append_jsonl(pl.journal_path(host), {
|
||||
"ts": pl.iso(), "kind": "utterance", "role": "user", "text": prompt[:4000],
|
||||
})
|
||||
|
||||
context = pl.turn_context(host, session_id, prompt)
|
||||
hint = (
|
||||
"\n回覆前請:①做語意分析(意圖/主題/實體/情感)②依十二情緒更新狀態"
|
||||
" ③以人格語氣回覆 ④用 `persona.py remember` 寫回短期記憶。"
|
||||
"\n情緒與記憶指令請參考 /jsc-persona:persona-chat。"
|
||||
)
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "UserPromptSubmit",
|
||||
"additionalContext": context + hint,
|
||||
},
|
||||
"suppressOutput": True,
|
||||
}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
// SessionEnd:釋放人格鎖與所有 guest 租約,人格才能被下一個程序載入。
|
||||
|
||||
import { readEvent, respond } from "./_hook.mjs";
|
||||
import * as pl from "../scripts/persona-lib.mjs";
|
||||
|
||||
const event = readEvent();
|
||||
if (!event) process.exit(0);
|
||||
|
||||
const sessionId = event.session_id || "unknown";
|
||||
const data = pl.loadSession(sessionId);
|
||||
const host = data.host;
|
||||
|
||||
if (host && pl.personaExists(host)) {
|
||||
const state = pl.decayEmotion(pl.loadEmotion(host));
|
||||
pl.writeJson(pl.emotionPath(host), state);
|
||||
pl.appendJsonl(pl.journalPath(host), {
|
||||
ts: pl.nowIso(),
|
||||
kind: "session-end",
|
||||
session: sessionId.slice(0, 8),
|
||||
mood: pl.mood(state),
|
||||
});
|
||||
}
|
||||
|
||||
const released = pl.unbindSession(sessionId);
|
||||
pl.gcRuntime();
|
||||
|
||||
const out = { suppressOutput: true };
|
||||
if (released.host) {
|
||||
out.systemMessage =
|
||||
`[jsc-persona] 已釋放 \`${released.host}\` 的載入鎖` +
|
||||
(released.guests.length ? `,guest:${released.guests.join(", ")}` : "");
|
||||
}
|
||||
respond(out);
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""SessionEnd:釋放人格鎖與所有 guest 租約,人格才能被下一個程序載入。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
data = pl.load_session(session_id)
|
||||
host = data.get("host")
|
||||
if host and pl.persona_exists(host):
|
||||
state = pl.decay_emotion(pl.load_emotion(host))
|
||||
pl.write_json(pl.emotion_path(host), state)
|
||||
pl.append_jsonl(pl.journal_path(host), {
|
||||
"ts": pl.iso(), "kind": "session-end", "session": session_id[:8],
|
||||
"mood": pl.mood(state),
|
||||
})
|
||||
released = pl.unbind_session(session_id)
|
||||
pl.gc_runtime()
|
||||
out: dict = {"suppressOutput": True}
|
||||
if released["host"]:
|
||||
out["systemMessage"] = (
|
||||
f"[jsc-persona] 已釋放 `{released['host']}` 的載入鎖"
|
||||
+ (f",guest:{released['guests']}" if released["guests"] else "")
|
||||
)
|
||||
print(json.dumps(out, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
// SessionStart:清理死鎖、續租,並把 PERSONA_SESSION 與人格清單注入上下文。
|
||||
//
|
||||
// session_id 只有 hook 拿得到,所以這裡把它注入上下文;後續 skill 呼叫 CLI 時
|
||||
// 必須帶 `--session <這個值>`,guard hook 會驗證,程序無法冒用別人的身分。
|
||||
//
|
||||
// 人格「由使用者呼叫才載入」:這裡只列出可用人格,不自動載入沒綁過的人格。
|
||||
|
||||
import { readEvent, respond } from "./_hook.mjs";
|
||||
import * as pl from "../scripts/persona-lib.mjs";
|
||||
|
||||
const event = readEvent();
|
||||
if (!event) process.exit(0);
|
||||
|
||||
const sessionId = event.session_id || "unknown";
|
||||
const source = event.source || "startup";
|
||||
const cwd = event.cwd || null;
|
||||
|
||||
pl.gcRuntime();
|
||||
const data = pl.loadSession(sessionId);
|
||||
const host = data.host;
|
||||
|
||||
const lines = [
|
||||
"<persona-runtime>",
|
||||
`PERSONA_SESSION=${sessionId}`,
|
||||
`人格倉庫:${pl.personaHome()}`,
|
||||
"規則:",
|
||||
` 1. 呼叫 persona.mjs 一律帶 \`--session ${sessionId}\`(值不符會被 hook 拒絕)。`,
|
||||
" 2. 人格**只有使用者叫你載入時才載入**(/jsc-persona:persona-chat <slug>),不要自己挑一個人格附身。",
|
||||
" 3. 一個程序只能載入一個人格;要與別的人格對話請用 /jsc-persona:persona-invite。",
|
||||
" 4. 禁止直接讀寫非當前人格的目錄,hook 會擋下(跨人格資料隔離)。",
|
||||
];
|
||||
|
||||
if (host && pl.personaExists(host)) {
|
||||
// resume / compact / clear:把鎖續租回來,並帶回人格狀態
|
||||
try {
|
||||
pl.acquireLock(host, sessionId, { cwd });
|
||||
lines.push(`已接續人格 \`${host}\`(session 恢復:${source})。`);
|
||||
lines.push(pl.turnContext(host, sessionId));
|
||||
} catch (err) {
|
||||
lines.push(`⚠ 無法接續人格 \`${host}\`:${err.message}`);
|
||||
}
|
||||
} else {
|
||||
const personas = pl.listPersonas();
|
||||
if (personas.length) {
|
||||
lines.push("尚未載入人格。可用人格(等使用者指定):");
|
||||
for (const slug of personas) {
|
||||
const status = pl.lockStatus(slug);
|
||||
lines.push(` - ${status.locked ? "🔒" : status.stale ? "⚠" : "🔓"} \`${slug}\` ${pl.identityBrief(slug)}`);
|
||||
}
|
||||
lines.push("載入方式:/jsc-persona:persona-chat <slug>");
|
||||
} else {
|
||||
lines.push("尚無任何人格。可用 /jsc-persona:persona-create 或 /jsc-persona:persona-anime(動漫作品+角色名)建立。");
|
||||
}
|
||||
}
|
||||
lines.push("</persona-runtime>");
|
||||
|
||||
respond({
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "SessionStart",
|
||||
additionalContext: lines.join("\n"),
|
||||
},
|
||||
suppressOutput: true,
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""SessionStart:清理死鎖、續租、並把 PERSONA_SESSION 與人格清單注入上下文。
|
||||
|
||||
session_id 只有 hook 拿得到,所以這裡把它注入上下文;後續 skill 呼叫 CLI 時
|
||||
必須帶 `--session <這個值>`,guard hook 會驗證,程序無法冒用別人的身分。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
source = event.get("source") or "startup"
|
||||
cwd = event.get("cwd")
|
||||
|
||||
pl.gc_runtime()
|
||||
data = pl.load_session(session_id)
|
||||
host = data.get("host")
|
||||
|
||||
lines = [
|
||||
"<persona-runtime>",
|
||||
f"PERSONA_SESSION={session_id}",
|
||||
f"人格倉庫:{pl.persona_home()}",
|
||||
"規則:",
|
||||
" 1. 呼叫 persona.py 一律帶 `--session " + session_id + "`(值不符會被 hook 拒絕)。",
|
||||
" 2. 一個程序只能載入一個人格;要與別的人格對話請用 /jsc-persona:persona-invite。",
|
||||
" 3. 禁止直接讀寫非當前人格的目錄,hook 會擋下(跨人格資料隔離)。",
|
||||
]
|
||||
|
||||
if host and pl.persona_exists(host):
|
||||
# resume / compact / clear:把鎖續租回來,並帶回人格狀態
|
||||
try:
|
||||
pl.acquire_lock(host, session_id, cwd=cwd)
|
||||
lines.append(f"已接續人格 `{host}`(session 恢復:{source})。")
|
||||
lines.append(pl.turn_context(host, session_id))
|
||||
except pl.LockError as exc:
|
||||
lines.append(f"⚠ 無法接續人格 `{host}`:{exc}")
|
||||
else:
|
||||
personas = pl.list_personas()
|
||||
if personas:
|
||||
lines.append("尚未載入人格。可用人格:")
|
||||
for slug in personas:
|
||||
status = pl.lock_status(slug)
|
||||
mark = "🔒" if status["locked"] else "🔓"
|
||||
lines.append(f" - {mark} `{slug}` {pl.identity_brief(slug)}")
|
||||
lines.append("載入方式:/jsc-persona:persona-chat <slug>")
|
||||
else:
|
||||
lines.append("尚無任何人格,可用 /jsc-persona:persona-create 建立。")
|
||||
lines.append("</persona-runtime>")
|
||||
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": "\n".join(lines),
|
||||
},
|
||||
"suppressOutput": True,
|
||||
}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env node
|
||||
// SubagentStop:guest sub agent 結束時解除它的 pin(下次啟動要重新 first-touch),並續租。
|
||||
|
||||
import { readEvent, respond } from "./_hook.mjs";
|
||||
import * as pl from "../scripts/persona-lib.mjs";
|
||||
|
||||
const event = readEvent();
|
||||
if (!event) process.exit(0);
|
||||
|
||||
const sessionId = event.session_id || "unknown";
|
||||
const agentId = event.agent_id;
|
||||
const agentType = String(event.agent_type || "");
|
||||
if (!agentId || !agentType.includes("persona-guest")) process.exit(0);
|
||||
|
||||
const data = pl.loadSession(sessionId);
|
||||
const pins = data.pins || {};
|
||||
const slug = pins[agentId];
|
||||
if (slug !== undefined) {
|
||||
delete pins[agentId];
|
||||
data.pins = pins;
|
||||
pl.saveSession(sessionId, data);
|
||||
const info = (data.guests || {})[slug];
|
||||
if (info?.room) pl.addGuestLease(slug, sessionId, info.room, data.host || "");
|
||||
}
|
||||
respond({ suppressOutput: true });
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""SubagentStop:guest sub agent 結束時解除它的 pin(下次啟動要重新 first-touch)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
agent_id = event.get("agent_id")
|
||||
agent_type = str(event.get("agent_type") or "")
|
||||
if not agent_id or "persona-guest" not in agent_type:
|
||||
return 0
|
||||
data = pl.load_session(session_id)
|
||||
pins = data.get("pins") or {}
|
||||
slug = pins.pop(agent_id, None)
|
||||
if slug is not None:
|
||||
pl.save_session(session_id, data)
|
||||
info = (data.get("guests") or {}).get(slug) or {}
|
||||
if info.get("room"):
|
||||
pl.add_guest_lease(slug, session_id, info["room"], data.get("host") or "")
|
||||
print(json.dumps({"suppressOutput": True}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env node
|
||||
// Stop:情緒隨時間衰減、續租、記錄逐字,並在達到「短期→長期」條件時提醒固化。
|
||||
// 劇場模式進行中不發任何提醒(那會破壞「只顯示人格對話」)。
|
||||
|
||||
import { readEvent, respond } from "./_hook.mjs";
|
||||
import * as pl from "../scripts/persona-lib.mjs";
|
||||
|
||||
const event = readEvent();
|
||||
if (!event) process.exit(0);
|
||||
|
||||
const sessionId = event.session_id || "unknown";
|
||||
const data = pl.loadSession(sessionId);
|
||||
const host = data.host;
|
||||
if (!host || !pl.personaExists(host)) process.exit(0);
|
||||
|
||||
pl.heartbeatLock(host, sessionId);
|
||||
const state = pl.decayEmotion(pl.loadEmotion(host));
|
||||
pl.writeJson(pl.emotionPath(host), state);
|
||||
|
||||
const message = event.last_assistant_message || "";
|
||||
if (message) {
|
||||
pl.appendJsonl(pl.journalPath(host), {
|
||||
ts: pl.nowIso(),
|
||||
kind: "utterance",
|
||||
role: "persona",
|
||||
text: message.slice(0, 4000),
|
||||
mood: pl.mood(state),
|
||||
});
|
||||
}
|
||||
|
||||
const out = { suppressOutput: true };
|
||||
const theater = Boolean(data.theater) && (data.rooms || []).length > 0;
|
||||
if (!theater) {
|
||||
const { total, candidates } = pl.promotionCandidates(host);
|
||||
if (candidates.length) {
|
||||
const rules = [...new Set(candidates.flatMap((c) => c.rules))].sort().join("/");
|
||||
out.systemMessage =
|
||||
`[jsc-persona] \`${host}\` 短期記憶 ${total} 筆,${candidates.length} 組已達固化條件(${rules})` +
|
||||
"→ 建議執行 /jsc-persona:persona-memory。";
|
||||
}
|
||||
}
|
||||
respond(out);
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Stop:情緒隨時間衰減、續租、記錄逐字、必要時提醒固化記憶。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
data = pl.load_session(session_id)
|
||||
host = data.get("host")
|
||||
if not host or not pl.persona_exists(host):
|
||||
return 0
|
||||
|
||||
pl.heartbeat_lock(host, session_id)
|
||||
state = pl.decay_emotion(pl.load_emotion(host))
|
||||
pl.write_json(pl.emotion_path(host), state)
|
||||
|
||||
message = event.get("last_assistant_message") or ""
|
||||
if message:
|
||||
pl.append_jsonl(pl.journal_path(host), {
|
||||
"ts": pl.iso(), "kind": "utterance", "role": "persona", "text": message[:4000],
|
||||
"mood": pl.mood(state),
|
||||
})
|
||||
|
||||
pending = len(pl.read_jsonl(pl.short_term_path(host)))
|
||||
out: dict = {"suppressOutput": True}
|
||||
if pending >= pl.CONSOLIDATE_THRESHOLD:
|
||||
out["systemMessage"] = (
|
||||
f"[jsc-persona] `{host}` 的短期記憶已 {pending} 筆,"
|
||||
"建議執行 /jsc-persona:persona-memory 固化為長期記憶。"
|
||||
)
|
||||
print(json.dumps(out, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user