From 8a4ca14b7cd34a29e2e573e4c0ba410377b33581 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 29 Jul 2026 18:39:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(role):=20=E6=8E=92=E7=A8=8B=E6=94=B9?= =?UTF-8?q?=E7=82=BA=E6=9C=8D=E5=8B=99=E6=89=80=E6=9C=89=E8=A7=92=E8=89=B2?= =?UTF-8?q?=EF=BC=8C=E4=B8=A6=E5=9C=A8=E6=95=B4=E7=90=86=E5=A4=B1=E6=95=97?= =?UTF-8?q?=E6=99=82=E9=87=8D=E8=A9=A6=E4=B8=80=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 西莉卡反映「每天睡覺的時候好像沒辦法睡」。查證後成立,而且比反映的更嚴重: 她從建立到現在(約 6 小時)一次都沒被整理過,另一個角色連 state.json 都還沒產生。 根因:--install-cron 把 ROLE_NAME 寫死進 crontab,排程只服務啟用角色(.active)。 其他角色的 inbox 只會累積,永遠等不到整理 —— 而且不會有任何錯誤訊息, 因為對排程而言它「成功地整理了那一個角色」。這是無聲失效,只有被漏掉的角色自己會發現。 - 新增 all_roles_enabled/sleep_target_roles/for_each_target_role - cron_env_prefix 在多角色模式下不寫 ROLE_NAME,改寫 ROLE_SLEEP_ALL_ROLES=1 - --run/--nap/--catchup/--brief 全部改為逐一處理目標角色; 睡眠時段與 AI 運行檢查移到迴圈外(與角色無關,只檢查一次), 小睡與補跑的條件仍是 per-role - 手動指定 ROLE_NAME 時行為不變;ROLE_SLEEP_ALL_ROLES=0 可退回舊的單角色模式 - --status 新增「排程涵蓋角色」,讀到舊條目寫死 ROLE_NAME 時直接標示警告 - 整理失敗改為重試一次:實測偶發 CLI 輸出空或 JSON 不合法,同批素材重跑即成功; 沒有重試時該角色要等下一個週期,多角色模式下代價更大 實測結果(三個角色循序): - LISBETH01 新增 4 則、關係史 +2(首次整理) - SILICA01 第一次套用失敗、重跑成功,新增 5 則、捨棄 2 則、關係史 +3(首次整理) - YUI01 判定不需補跑,正確略過 Co-Authored-By: Claude Opus 5 (1M context) --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- scripts/role/role_sleep.sh | 164 ++++++++++++++++++++++++++++++------- skills/role/SKILL.md | 19 +++++ 5 files changed, 156 insertions(+), 33 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 90e8564..e0d4f13 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-generic", - "version": "0.1.2", + "version": "0.1.3", "description": "JSC 跨 AI 助理共用規範 plugin(Claude Code / Codex / Antigravity / OpenCode)。所有 skills 以 SKILL.md 為共通標準,於 Claude Code 以 /jsc-generic: 前綴呼叫。", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index b6bc3d9..bd1450a 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-generic", - "version": "0.1.2", + "version": "0.1.3", "description": "JSC 跨 AI 助理共用規範 plugin。所有 skills 以 SKILL.md 為共通標準。", "skills": "./skills" } diff --git a/plugin.json b/plugin.json index d544d7b..901b479 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-generic", - "version": "0.1.2", + "version": "0.1.3", "description": "JSC 跨 AI 助理共用規範 plugin。所有 skills 以 SKILL.md 為共通標準;於 Antigravity 以 /jsc-generic: 前綴呼叫。", "skills": "./skills/" } diff --git a/scripts/role/role_sleep.sh b/scripts/role/role_sleep.sh index 9c8c8be..6781557 100755 --- a/scripts/role/role_sleep.sh +++ b/scripts/role/role_sleep.sh @@ -7,7 +7,7 @@ # 另提供 --nap(CLI 閒置時的小睡整理)、--catchup(cron 未執行時的補跑)、 # --force(手動立即整理)、--export(匯出角色壓縮檔)、 # --install-cron/--remove-cron(排程安裝與移除)、--status(狀態)。 -# 更新時間:2026/07/29 18:16:18 +# 更新時間:2026/07/29 18:39:54 # 相依:bash、node、任一 headless CLI、crontab(僅排程安裝需要)、 # 同目錄的 role_lib.sh 與 memory.js。 # 退出碼:0 成功或無事可做;1 參數錯誤或整理失敗(cron 觸發時不影響使用者)。 @@ -202,22 +202,30 @@ ${material} EOF_PROMPT )" - result="$(role_run_cli "$cli" "$prompt" "$SLEEP_TIMEOUT")" - if [ -z "$result" ]; then - role_log "ERR" "整理結果為空(CLI ${cli}),保留待整理記憶到下個週期" - role_lock_release "$ROLE" - trap - EXIT - return 1 - fi - - result="$(printf '%s' "$result" | head -c "$SLEEP_OUTPUT_LIMIT" | node "${SCRIPT_DIR}/transcript.js" redact 2>/dev/null)" - applied="$(printf '%s' "$result" | node "${SCRIPT_DIR}/memory.js" apply --role "$ROLE" 2>/dev/null)" + # 重試一次:實測整理會偶發失敗(CLI 輸出空或 JSON 不合法),素材本身沒問題 —— + # 同一批素材重跑就成功。沒有重試時,該角色要等到下一個週期才會再被整理; + # 多角色模式下這個代價更大(一個角色失敗不該讓它整晚都沒睡)。 + local attempt=1 max_attempts=2 + applied="" + while [ "$attempt" -le "$max_attempts" ]; do + result="$(role_run_cli "$cli" "$prompt" "$SLEEP_TIMEOUT")" + if [ -z "$result" ]; then + role_log "WRN" "整理結果為空(CLI ${cli},第 ${attempt}/${max_attempts} 次)" + else + result="$(printf '%s' "$result" | head -c "$SLEEP_OUTPUT_LIMIT" | node "${SCRIPT_DIR}/transcript.js" redact 2>/dev/null)" + applied="$(printf '%s' "$result" | node "${SCRIPT_DIR}/memory.js" apply --role "$ROLE" 2>/dev/null)" + [ -n "$applied" ] && break + role_log "WRN" "整理結果無法套用(角色 ${ROLE},第 ${attempt}/${max_attempts} 次)" + fi + attempt=$((attempt + 1)) + done if [ -z "$applied" ]; then - role_log "ERR" "整理結果無法套用(角色 ${ROLE}),保留待整理記憶到下個週期" + role_log "ERR" "整理失敗 ${max_attempts} 次(角色 ${ROLE}),保留待整理記憶到下個週期" role_lock_release "$ROLE" trap - EXIT return 1 fi + [ "$attempt" -gt 1 ] && role_log "INF" "第 ${attempt} 次嘗試成功(角色 ${ROLE})" role_log "INF" "記憶整理完成(角色 ${ROLE},觸發:${reason}):${applied}" forgotten="$(node "${SCRIPT_DIR}/memory.js" forget --role "$ROLE" 2>/dev/null | tr '\n' ';')" @@ -248,6 +256,63 @@ cron_path_append() { esac } +all_roles_enabled() { + # 排程是否服務所有角色(預設是)。設 ROLE_SLEEP_ALL_ROLES=0 可退回只服務啟用角色。 + case "${ROLE_SLEEP_ALL_ROLES:-1}" in + 0|false|no) return 1 ;; + esac + return 0 +} + +sleep_target_roles() { + # 本次要處理哪些角色。 + # + # 為什麼需要這個:舊版把 ROLE_NAME 寫死進 crontab,於是只有「啟用角色」會睡, + # 其他角色的 inbox 永遠累積、state.json 連 last_sleep 都不會出現 —— 而且不會有任何錯誤訊息, + # 因為對排程而言它「成功地整理了那一個角色」。多角色系統必須掃描角色目錄。 + # + # 明確指定 ROLE_NAME 時仍只處理該角色,手動操作與單角色模式的行為不變。 + if ! all_roles_enabled || [ -n "${ROLE_NAME:-}" ]; then + printf '%s\n' "$(role_resolve_name)" + return 0 + fi + local home file id seen="" + home="$(role_home)" + [ -d "$home" ] || return 0 + for file in "$home"/*.identity.md "$home"/*.md; do + [ -f "$file" ] || continue + case "$file" in + *.soul.md) continue ;; # soul 不是主定義檔 + *.identity.md) id="$(basename "$file" .identity.md)" ;; + *) id="$(basename "$file" .md)" + [ -f "$(role_identity_file "$id")" ] && continue ;; # 已有新格式就不重複列 + esac + case " ${seen} " in *" ${id} "*) continue ;; esac + seen="${seen} ${id}" + printf '%s\n' "$id" + done +} + +for_each_target_role() { + # 對每個目標角色依序執行 $1;角色鎖是 per-role,循序執行不會互相搶鎖。 + local fn="$1" r handled=0 + for r in $(sleep_target_roles); do + [ -n "$r" ] || continue + ROLE="$r" + if [ ! -f "$(role_file "$ROLE")" ]; then + role_log "DBG" "略過 ${ROLE}:找不到角色定義檔" + continue + fi + handled=$((handled + 1)) + "$fn" + done + if [ "$handled" = "0" ]; then + role_log "WRN" "沒有可處理的角色(角色目錄:$(role_home))" + return 1 + fi + return 0 +} + cron_path() { # cron 只需要系統工具、node 與摘要 CLI;避免把互動 shell 的超長 PATH 原樣寫入。 local value="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" @@ -265,8 +330,15 @@ cron_path() { cron_env_prefix() { # 組出 cron 需要的精簡環境變數 local env_prefix="PATH=$(cron_quote "$(cron_path)")" - local var - for var in ROLE_ENABLED ROLE_NAME ROLE_HOME ROLE_MEMORY_HOME ROLE_CLI ROLE_MODEL ROLE_SLEEP_START ROLE_SLEEP_END ROLE_SCOPE ROLE_NAP_ENABLED ROLE_NAP_IDLE_MINUTES ROLE_NAP_MIN_INBOX ROLE_NAP_INTERVAL_MINUTES; do + local var vars="ROLE_ENABLED ROLE_HOME ROLE_MEMORY_HOME ROLE_CLI ROLE_MODEL ROLE_SLEEP_START ROLE_SLEEP_END ROLE_SCOPE ROLE_NAP_ENABLED ROLE_NAP_IDLE_MINUTES ROLE_NAP_MIN_INBOX ROLE_NAP_INTERVAL_MINUTES" + # 多角色模式(預設)刻意**不寫入 ROLE_NAME** —— 寫死角色會讓其他角色永遠不被整理。 + # 單角色模式才綁定 ROLE_NAME。 + if all_roles_enabled; then + env_prefix="${env_prefix} ROLE_SLEEP_ALL_ROLES=1" + else + vars="ROLE_NAME ${vars}" + fi + for var in $vars; do if [ -n "${!var:-}" ]; then env_prefix="${env_prefix} ${var}=$(cron_quote "${!var}")" fi @@ -463,6 +535,33 @@ EOF_LAUNCHER return 0 } +cycle_for_cron() { + # cron 觸發的單一角色整理(時段與 AI 檢查已在呼叫端做過,與角色無關) + sleep_cycle "cron" +} + +cycle_for_nap() { + # 小睡條件是 per-role 的(各自的閒置時間與 inbox 筆數) + if [ "$(node "${SCRIPT_DIR}/memory.js" need-nap --role "$ROLE" --idle-minutes "$(nap_idle_minutes)" --min-inbox "$(nap_min_inbox)" 2>/dev/null)" != "yes" ]; then + role_log "DBG" "${ROLE} 尚未達小睡條件(閒置滿 $(nap_idle_minutes) 分鐘且 inbox ≥ $(nap_min_inbox) 則),略過" + return 0 + fi + sleep_cycle "小睡" +} + +cycle_for_catchup() { + if [ "$(node "${SCRIPT_DIR}/memory.js" need-sleep --role "$ROLE" 2>/dev/null)" != "yes" ]; then + role_log "DBG" "${ROLE} 不需補跑整理" + return 0 + fi + sleep_cycle "補跑" +} + +cycle_for_brief() { + # run_brief 內部會在沒有 <角色>.checks 目錄時自行略過,沒設定的角色零影響 + run_brief +} + install_cron() { # 安裝或更新睡眠排程;以 marker 註解辨識自己的條目,不動使用者其他排程 command -v crontab >/dev/null 2>&1 || { role_log "ERR" "找不到 crontab,無法安裝排程"; return 1; } @@ -726,6 +825,22 @@ cron_target_state() { fi } +cron_scope_state() { + # 排程實際涵蓋哪些角色。舊條目把 ROLE_NAME 寫死,只有那一個角色會睡, + # 其餘角色不會有任何錯誤訊息 —— 這裡把它顯示出來,避免又是無聲失效。 + local line pinned + line="$(crontab -l 2>/dev/null | grep -F "$CRON_MARKER" | head -n 1)" + [ -n "$line" ] || { printf '未安裝'; return 0; } + pinned="$(printf '%s' "$line" | sed -n "s/.*ROLE_NAME='\([^']*\)'.*/\1/p")" + if [ -n "$pinned" ]; then + printf '⚠ 只有 %s(條目寫死 ROLE_NAME,其他角色不會被整理;重跑 --install-cron 可改為全部)' "$pinned" + elif printf '%s' "$line" | grep -q 'ROLE_SLEEP_ALL_ROLES=1'; then + printf '全部(%s)' "$(sleep_target_roles | tr '\n' ' ' | sed 's/ $//')" + else + printf '未指定(腳本會退回啟用角色 %s)' "$(role_resolve_name)" + fi +} + show_status() { # 以表格輸出目前角色與記憶狀態(供 skill 的 --status 使用) local cron_state="未安裝" nap_state="未安裝" brief_state="未安裝" cron_service="未執行" window="否" checks_state instance_state @@ -764,6 +879,7 @@ show_status() { printf '| 小睡排程 | %s |\n' "$nap_state" printf '| 晨間檢查排程 | %s |\n' "$brief_state" printf '| 排程指向 | %s |\n' "$(cron_target_state)" + printf '| 排程涵蓋角色 | %s |\n' "$(cron_scope_state)" printf '| 角色載入鎖 | %s |\n' "$instance_state" printf '| 檢查腳本目錄 | %s |\n' "$checks_state" printf '| 小睡啟用 | %s |\n' "$(nap_enabled && printf '是' || printf '否')" @@ -863,7 +979,6 @@ MODE="${1:---status}" case "$MODE" in --run) role_enabled || exit 0 - require_role if ! role_in_sleep_window; then role_log "DBG" "目前不在睡眠時段($(role_sleep_start)–$(role_sleep_end)),略過" exit 0 @@ -872,11 +987,10 @@ case "$MODE" in role_log "INF" "偵測到 AI 正在運行,本小時不進入睡眠,下個整點再檢查" exit 0 fi - sleep_cycle "cron" + for_each_target_role cycle_for_cron ;; --nap) role_enabled || exit 0 - require_role if ! nap_enabled; then role_log "DBG" "小睡已停用(ROLE_NAP_ENABLED=${ROLE_NAP_ENABLED:-1}),略過" exit 0 @@ -885,20 +999,11 @@ case "$MODE" in role_log "INF" "偵測到整理用 headless CLI 正在運行,本次小睡略過" exit 0 fi - if [ "$(node "${SCRIPT_DIR}/memory.js" need-nap --role "$ROLE" --idle-minutes "$(nap_idle_minutes)" --min-inbox "$(nap_min_inbox)" 2>/dev/null)" != "yes" ]; then - role_log "DBG" "尚未達小睡條件(閒置滿 $(nap_idle_minutes) 分鐘且 inbox ≥ $(nap_min_inbox) 則),略過" - exit 0 - fi - sleep_cycle "小睡" + for_each_target_role cycle_for_nap ;; --catchup) role_enabled || exit 0 - require_role - if [ "$(node "${SCRIPT_DIR}/memory.js" need-sleep --role "$ROLE" 2>/dev/null)" != "yes" ]; then - role_log "DBG" "不需補跑整理" - exit 0 - fi - sleep_cycle "補跑" + for_each_target_role cycle_for_catchup ;; --force) require_role @@ -924,12 +1029,11 @@ case "$MODE" in ;; --brief) role_enabled || exit 0 - require_role if ! brief_enabled; then role_log "DBG" "晨間狀態檢查已停用(ROLE_BRIEF_ENABLED=${ROLE_BRIEF_ENABLED:-1}),略過" exit 0 fi - run_brief + for_each_target_role cycle_for_brief ;; --export) if [ -n "${3:-}" ]; then diff --git a/skills/role/SKILL.md b/skills/role/SKILL.md index e4f7a0a..4bc09c3 100644 --- a/skills/role/SKILL.md +++ b/skills/role/SKILL.md @@ -124,6 +124,7 @@ ROLE_DIR="/../../scripts/role" # 其他助理 | `ROLE_NAP_IDLE_MINUTES` | | 小睡前需連續閒置的分鐘數,由 Stop hook 記錄最後互動時間 | `45` | | `ROLE_NAP_MIN_INBOX` | | 小睡整理所需的最少待整理 inbox 筆數 | `3` | | `ROLE_NAP_INTERVAL_MINUTES` | | 小睡排程檢查間隔分鐘數(cron 每 `*/N` 分鐘觸發) | `10` | +| `ROLE_SLEEP_ALL_ROLES` | | 排程是否服務**所有**角色(`0` 則只服務 `ROLE_NAME`/`.active` 的啟用角色)。`--install-cron` 會依此決定 crontab 條目要不要寫死 `ROLE_NAME` | `1` | | `ROLE_BRIEF_ENABLED` | | 晨間狀態檢查開關;設 `0` 可停用 | `1` | | `ROLE_BRIEF_TIMEOUT` | | 單個檢查腳本的逾時秒數 | `30` | | `ROLE_BRIEF_EACH_LIMIT` | | 單個檢查腳本輸出的字元上限 | `600` | @@ -781,6 +782,24 @@ updated: 判準刻意放在 `relevance` 而非型態:並非所有事件與知識都該優待 —— **沒有情感脈絡的一次性工作進度仍照原規則淡去**,被留下的是帶著溫度的那些。因此兩份 prompt 都明訂:只要內容承載情感、關係溫度或當時的心情,`relevance` **必須**含 `emotional`;`episodic` 與 `semantic` 最容易漏標,漏了就會被當成一般進度處理。 +### 排程涵蓋所有角色 + +睡眠、小睡與晨間檢查排程**預設服務角色目錄下的每一個角色**,而不只是 `.active` 指定的啟用角色。 + +**修正前的錯誤**:`--install-cron` 會把 `ROLE_NAME` 寫死進 crontab,於是只有啟用角色會睡。其他角色的 `inbox/` 永遠累積、`state.json` 連 `last_sleep` 都不會出現 —— 而且**不會有任何錯誤訊息**,因為對排程而言它「成功地整理了那一個角色」。實測有兩個角色從建立起完全沒被整理過。 + +| 模式 | crontab 條目 | 行為 | +| --- | --- | --- | +| 多角色(預設) | 寫入 `ROLE_SLEEP_ALL_ROLES=1`,**不寫 `ROLE_NAME`** | 掃描 `~/.roles/*.identity.md` 逐一整理 | +| 單角色 | `ROLE_SLEEP_ALL_ROLES=0` 時寫入 `ROLE_NAME` | 只整理該角色(舊行為) | + +- 手動執行時若指定 `ROLE_NAME`,仍只處理該角色 —— 手動操作行為不變。 +- 角色鎖是 per-role,多角色**循序**執行不會互相搶鎖;睡眠時段與「是否有 AI 在運行」只檢查一次。 +- 沒有 `<角色 ID>.checks/` 目錄的角色會自動跳過晨間檢查,對沒設定的角色零影響。 +- `--status` 新增「排程涵蓋角色」欄位:若讀到舊條目寫死 `ROLE_NAME`,會直接標示警告。 + +**整理失敗會重試一次**:實測整理偶發失敗(CLI 輸出空或 JSON 不合法),同一批素材重跑即成功。沒有重試時該角色要等下一個週期,多角色模式下代價更大。 + 遺忘規則(只套用於日常與其他): | 分類 | 未更新天數 | 命中次數 | 優先度 | 關聯 | 動作 | -- 2.53.0