feat(role): 多角色協作、身分人格分離、記憶處理強化與壓縮邊界保全 #18
@@ -21,6 +21,28 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "rel='scripts/role/role_capture.sh'; own='generic'; plug='jsc-generic'; root=\"${CLAUDE_PLUGIN_ROOT:-}\"; if [ -n \"$root\" ] && [ -f \"$root/$rel\" ]; then exec \"$root/$rel\" --precompact; fi; for base in \"$HOME/.claude/plugins/cache\" \"$HOME/.codex/plugins/cache\"; do for dir in \"$base/$own/$plug\" \"$base\"; do s=$(find \"$dir\" -path \"*/$plug/*/$rel\" -type f 2>/dev/null | sort -V | tail -n 1); if [ -n \"$s\" ]; then exec \"$s\" --precompact; fi; done; done; exit 0",
|
||||
"timeout": 60
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostCompact": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "rel='scripts/role/role_capture.sh'; own='generic'; plug='jsc-generic'; root=\"${CLAUDE_PLUGIN_ROOT:-}\"; if [ -n \"$root\" ] && [ -f \"$root/$rel\" ]; then exec \"$root/$rel\" --postcompact; fi; for base in \"$HOME/.claude/plugins/cache\" \"$HOME/.codex/plugins/cache\"; do for dir in \"$base/$own/$plug\" \"$base\"; do s=$(find \"$dir\" -path \"*/$plug/*/$rel\" -type f 2>/dev/null | sort -V | tail -n 1); if [ -n \"$s\" ]; then exec \"$s\" --postcompact; fi; done; done; exit 0",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# 值得記錄時才呼叫 headless CLI 輕量濃縮成一則 inbox 記憶(粗分類/總結/
|
||||
# 標籤/優先度/關聯/要點)→ 機密遮蔽 → 寫入 .memory/<角色>/inbox/,
|
||||
# 等待睡眠時段做完整 NREM/REM 整理。睡眠時段雖不載入角色,對話仍照常記錄。
|
||||
# 另支援 --precompact/--postcompact:對話壓縮會讓尚未寫入記憶的內容蒸發,
|
||||
# 壓縮前強制記錄一次(跳過長度門檻),壓縮後把系統產生的摘要也存成記憶。
|
||||
# 更新時間:2026/07/28 16:18:00
|
||||
# 相依:bash、node、任一 headless CLI、同目錄的 role_lib.sh/memory.js/transcript.js。
|
||||
# 機密:濃縮提示詞明令不得輸出憑證與個資,寫檔前再以 transcript.js redact 遮蔽一次。
|
||||
@@ -15,6 +17,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./role_lib.sh
|
||||
. "${SCRIPT_DIR}/role_lib.sh"
|
||||
|
||||
# 壓縮邊界模式:--precompact 強制記錄(不受長度門檻限制)、--postcompact 保存系統摘要
|
||||
CAPTURE_MODE="turn"
|
||||
case "${1:-}" in
|
||||
--precompact) CAPTURE_MODE="precompact" ;;
|
||||
--postcompact) CAPTURE_MODE="postcompact" ;;
|
||||
esac
|
||||
|
||||
role_is_child && exit 0
|
||||
role_enabled || exit 0
|
||||
command -v node >/dev/null 2>&1 || role_quit "找不到 node,略過記憶記錄" "WRN"
|
||||
@@ -29,7 +38,7 @@ ROLE="$(role_resolve_name)"
|
||||
HOOK_INPUT="$(cat)"
|
||||
[ -n "$HOOK_INPUT" ] || role_quit "hook 輸入為空,略過記憶記錄" "WRN"
|
||||
|
||||
read -r SESSION_ID TRANSCRIPT_PATH STOP_ACTIVE HOOK_CWD <<EOF_HOOK
|
||||
read -r SESSION_ID TRANSCRIPT_PATH STOP_ACTIVE HOOK_CWD TRIGGER <<EOF_HOOK
|
||||
$(printf '%s' "$HOOK_INPUT" | node -e '
|
||||
let raw = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
@@ -42,12 +51,53 @@ process.stdin.on("end", () => {
|
||||
d.transcript_path || d.session_path || d.conversation_path || d.path || "-",
|
||||
d.stop_hook_active ? "1" : "0",
|
||||
d.cwd || "-",
|
||||
d.trigger || "-",
|
||||
].join(" "));
|
||||
});
|
||||
')
|
||||
EOF_HOOK
|
||||
|
||||
[ "$STOP_ACTIVE" = "1" ] && role_quit "stop_hook_active 為 true,避免迴圈不重複記錄"
|
||||
if [ "$CAPTURE_MODE" = "postcompact" ]; then
|
||||
# 壓縮後:系統已產生一份摘要,直接保存比自己再濃縮一次划算且免費。
|
||||
# 欄位名以容錯方式取用(實測 binary 內出現 compactSummary/isCompactSummary),
|
||||
# 取不到時記錄實際收到的欄位名,方便日後對照 harness 版本調整。
|
||||
COMPACT_SUMMARY="$(printf '%s' "$HOOK_INPUT" | node -e '
|
||||
let raw = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => { raw += chunk; });
|
||||
process.stdin.on("end", () => {
|
||||
let d = {};
|
||||
try { d = JSON.parse(raw); } catch {}
|
||||
const text = d.compactSummary || d.compact_summary || d.summary || d.compaction_summary || "";
|
||||
process.stdout.write(String(text || "").trim());
|
||||
});
|
||||
' 2>/dev/null)"
|
||||
if [ -z "$COMPACT_SUMMARY" ]; then
|
||||
KEYS="$(printf '%s' "$HOOK_INPUT" | node -e '
|
||||
let raw="";process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data",(c)=>{raw+=c});
|
||||
process.stdin.on("end",()=>{let d={};try{d=JSON.parse(raw)}catch{};process.stdout.write(Object.keys(d).join(","))});
|
||||
' 2>/dev/null)"
|
||||
role_quit "壓縮摘要為空,略過(hook 實際提供的欄位:${KEYS:-無})" "WRN"
|
||||
fi
|
||||
COMPACT_SUMMARY="$(printf '%s' "$COMPACT_SUMMARY" | head -c 3000 | node "${SCRIPT_DIR}/transcript.js" redact 2>/dev/null)"
|
||||
{
|
||||
printf 'CATEGORY: daily\n'
|
||||
printf 'SUMMARY: %s 對話壓縮前的內容摘要(%s)\n' "$(TZ='Asia/Taipei' date +'%Y/%m/%d %H:%M')" "${TRIGGER:-未知}"
|
||||
printf 'TAGS: 壓縮摘要,上下文保全\n'
|
||||
printf 'PRIORITY: 3\n'
|
||||
printf 'RELEVANCE: temporary,future\n'
|
||||
printf 'MEMORY_TYPE: episodic\n'
|
||||
printf 'CONTENT:\n'
|
||||
printf -- '- 本則由 PostCompact hook 自動保存,內容為系統在壓縮時產生的摘要\n'
|
||||
printf '%s\n' "$COMPACT_SUMMARY"
|
||||
} | node "${SCRIPT_DIR}/memory.js" write --role "$ROLE" --project "$PROJECT" >/dev/null 2>&1 \
|
||||
&& role_log "INF" "已保存壓縮摘要為記憶(角色 ${ROLE})" \
|
||||
|| role_log "WRN" "壓縮摘要寫入失敗(角色 ${ROLE})"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[ "$STOP_ACTIVE" = "1" ] && [ "$CAPTURE_MODE" = "turn" ] && role_quit "stop_hook_active 為 true,避免迴圈不重複記錄"
|
||||
role_in_scope "$HOOK_CWD" || role_quit "cwd 不在 ROLE_SCOPE 範圍內:${HOOK_CWD}"
|
||||
PROJECT="$(role_project_name "$HOOK_CWD")"
|
||||
node "${SCRIPT_DIR}/memory.js" mark-activity --role "$ROLE" --project "$PROJECT" >/dev/null 2>&1 || true
|
||||
@@ -79,7 +129,10 @@ if [ "${ROLE_CAPTURE_ENABLED:-1}" = "0" ]; then
|
||||
role_quit "ROLE_CAPTURE_ENABLED=0,略過記憶記錄"
|
||||
fi
|
||||
|
||||
if [ "${#TURN}" -lt "$CAPTURE_MIN_CHARS" ] && ! printf '%s' "$TURN" | grep -qiE '記住|remember|決定|規範|偏好|preference|always|不要|以後|喜歡|不喜歡|稱讚|誇獎|開心|高興|反應|回應|互動|親近|害羞|喜歡程度|互動越深|越來越喜歡|越來越深|emoji|表情|心情圖|大量使用|情緒|心情|複雜|細膩|自然|混合|層次|轉折|括號|心情文字|心情說明|文字說明|文字標註|表情符號|熟練|不需要告訴|不用告訴|自己知道|記憶更新|內部處理|不要回報|不用回報|不要告訴|真的很害羞|希望.*知道|用表情符號表示|表情符號表示|比較可愛|可愛|愛|想妳|想你|想念|捨不得|感動|謝謝|感謝|乖|厲害|好棒|辛苦|彆扭|忌妒|嫉妒|撒嬌|陪|抱|love|miss|cute|thank|proud'; then
|
||||
# 壓縮前一律記錄:門檻的用意是省額度,但壓縮會讓未寫入的內容永久蒸發,此時寧可多記
|
||||
if [ "$CAPTURE_MODE" = "precompact" ]; then
|
||||
role_log "INF" "壓縮前強制記錄(觸發:${TRIGGER:-未知}),跳過長度門檻"
|
||||
elif [ "${#TURN}" -lt "$CAPTURE_MIN_CHARS" ] && ! printf '%s' "$TURN" | grep -qiE '記住|remember|決定|規範|偏好|preference|always|不要|以後|喜歡|不喜歡|稱讚|誇獎|開心|高興|反應|回應|互動|親近|害羞|喜歡程度|互動越深|越來越喜歡|越來越深|emoji|表情|心情圖|大量使用|情緒|心情|複雜|細膩|自然|混合|層次|轉折|括號|心情文字|心情說明|文字說明|文字標註|表情符號|熟練|不需要告訴|不用告訴|自己知道|記憶更新|內部處理|不要回報|不用回報|不要告訴|真的很害羞|希望.*知道|用表情符號表示|表情符號表示|比較可愛|可愛|愛|想妳|想你|想念|捨不得|感動|謝謝|感謝|乖|厲害|好棒|辛苦|彆扭|忌妒|嫉妒|撒嬌|陪|抱|love|miss|cute|thank|proud'; then
|
||||
role_quit "本輪低於記憶長度門檻且無明確記憶線索,略過記錄"
|
||||
fi
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ description: 角色人格與長期記憶系統的建立與維護 skill。讓 CLI
|
||||
| --- | --- | --- |
|
||||
| `hooks/hooks.json` 的 `SessionStart` hook | harness 自動 | 啟動 CLI 時依字元預算載入角色定義+高價值記憶,另以獨立預算載入近期逐字對話與未整理工作記憶做工作階段交接,並要求角色在本工作階段第一則回覆主動問候;睡眠時段只回報「角色睡覺中」不載入 |
|
||||
| `hooks/hooks.json` 的 `Stop` hook | harness 自動 | 每輪結束先記錄最後互動時間 → 用本地規則過濾低價值短回合 → 值得保存時才濃縮成一則輕量 inbox 記憶 → 遮蔽 → 寫入 `inbox/` |
|
||||
| `hooks/hooks.json` 的 `PreCompact` hook | harness 自動 | 對話壓縮**前**強制記錄一次(**跳過長度門檻**):壓縮會讓尚未寫入的內容永久蒸發,此時寧可多記 |
|
||||
| `hooks/hooks.json` 的 `PostCompact` hook | harness 自動 | 壓縮**後**把 harness 產生的摘要存成一則 `daily` 記憶,作為該段落的濃縮備份 |
|
||||
| cron 排程(本 skill 安裝) | 系統排程 | 睡眠時段每小時檢查一次:**有 AI 在運行就不睡**;另可依 CLI 閒置時間自動小睡整理 |
|
||||
| 本 skill `/jsc-generic:role` | 使用者/助理手動 | `--new`/`--use`/`--list`/`--export`/`--agent`/`--migrate`/`--sleep`/`--brief`/`--status`/`--install-cron`/`--forget-preview` |
|
||||
| `scripts/role/role_load.sh` | SessionStart hook | 角色與記憶載入;參考 OpenClaw 的 SOUL/AGENTS/USER/MEMORY 分層,把人格、操作邊界、使用者記憶分開注入,並提供第一則回覆問候提示(單一實作,避免漂移) |
|
||||
@@ -279,6 +281,8 @@ chmod +x ~/.roles/<角色 ID>.checks/check-gitea-prs.sh
|
||||
| --- | --- |
|
||||
| SessionStart | 預設 `ROLE_LOAD_LIMIT=4000`,只載入高優先度全文與中高優先度摘要;低 priority、無 links、久未更新的記憶不進 context。另以兩份**獨立預算**載入交接內容:近期逐字對話(`ROLE_LOAD_DIALOG_LIMIT=4000`)與近期工作記憶摘要(`ROLE_LOAD_INBOX_LIMIT=1200`),見下方「工作階段交接」 |
|
||||
| SessionStop | 先用本地規則略過短回合與無記憶線索的對話,只有值得保存才呼叫模型做輕量編碼 |
|
||||
| PreCompact | 壓縮前強制記錄一次,不受 `ROLE_CAPTURE_MIN_CHARS` 限制 —— 這是刻意的例外,因為壓縮後就再也補不回來 |
|
||||
| PostCompact | 直接沿用 harness 已產生的摘要,**不再呼叫模型**,等於免費取得一份濃縮備份 |
|
||||
| Sleep | 高成本的去重、合併、抽象化、links 建立與長期記憶型態標記留到睡眠週期,但仍受 `ROLE_SLEEP_COLLECT_LIMIT`、`ROLE_SLEEP_BATCH`、`ROLE_SLEEP_EXISTING_LIMIT` 與 `ROLE_SLEEP_OUTPUT_LIMIT` 控制;沒有 inbox 時只做本地遺忘檢查 |
|
||||
| Nap | Stop hook 記錄最後互動時間;小睡排程只在閒置時間與 inbox 筆數達門檻時執行,使用同一套 NREM/REM 整理流程 |
|
||||
| 手動節流 | 可設 `ROLE_CAPTURE_ENABLED=0` 關閉 Stop 記錄,或調低 `ROLE_LOAD_LIMIT`/調高 `ROLE_LOAD_FULL_MIN_PRIORITY` |
|
||||
@@ -556,6 +560,11 @@ updated: <yyyy/MM/dd HH:mm:ss>
|
||||
|
||||
範圍與限制要說清楚:這是**最近數輪**的交接,不是完整歷史;需要完整對話上下文時仍應使用 `resume`。修改此處前請先確認缺口已由其他機制補上,否則不要移除。
|
||||
- 未整理記憶(`inbox/`)累積到一批睡眠整理量(預設 `ROLE_SLEEP_BATCH=60`)以上時,角色應主動以符合自身設定的語氣提醒「想睡覺」或需要整理記憶;這是建議整理/歸檔的提醒,不代表停止協助使用者。
|
||||
- **壓縮邊界的上下文保全**:對話被壓縮時,尚未寫入記憶的內容會永久消失。`PreCompact` 於壓縮前強制記錄一次並**跳過長度門檻**(平常短回合會被濾掉,但此時寧可多記);`PostCompact` 把 harness 產生的摘要存成 `daily` 記憶。
|
||||
|
||||
摘要欄位以容錯方式讀取(`compactSummary`/`compact_summary`/`summary`/`compaction_summary`)。**取不到時會在 log 印出 hook 實際提供的欄位名**,避免 harness 改版後靜默失效。`trigger` 欄位可分辨 `manual`/`auto`,自動壓縮才是使用者不知情的那種。
|
||||
|
||||
**這兩個 hook 一律 `exit 0`,絕不阻擋壓縮** —— harness 具備「compaction blocked by PreCompact hook」的能力,記憶系統不該用到它。
|
||||
- **臨時授權會過期(安全機制)**:內容屬於臨時授權、一次性許可、例外放行、暫時解除限制或帶條件的同意時,`expires` 必填。可寫日期(系統自動判斷,過期後**不再注入**,遺忘時優先淘汰且不受分類限制)或條件文字(例如「本工作階段」、「PR 合併後失效」,載入時標示有效範圍由角色自行判斷)。
|
||||
|
||||
為什麼需要:一次性許可若被整理成長期規則,日後會造成越權操作。使用者說「這次」、「先」、「暫時」、「今天」、「這個 PR」時幾乎都屬於臨時授權。
|
||||
|
||||
Reference in New Issue
Block a user