sync: HP_BOWEI 2026-10-20

This commit is contained in:
Myfro9
2026-10-20 17:39:07 +08:00
parent 798e92717a
commit 7ace9b332b
3 changed files with 147 additions and 0 deletions
+17
View File
@@ -108,6 +108,23 @@ worker 返回后,确认报告文件确实存在;不存在就把 worker 的
用户可能在启动时或中途说"不要停下来问我"、"所有问题主 agent 自己定"。这时调度员把这句话记入 `Rnn-user.md`,并告诉大脑:**ASK_USER 状态不再可用**,worker 报告里"需要用户决定"的点和大脑原本想问的事项全部由大脑裁定。为了用户事后能复核,要求大脑在每轮"分析"里把替用户做的裁定单列为"大脑裁定(原拟问用户)",并在 DONE 的总结里汇总这些裁定及依据。调度员自己也不再用 AskUserQuestion,但仍照常向用户打印名册和进展。
## 30 分钟巡检:不让子 agent 空等
Worker 在等待长任务(仿真、编译、Monitor)时很容易"睡死":等待事件到期或触发后它没有被唤醒,就一直停在那里,没人知道。实际运行里出现过 worker 停了 5 个多小时、仿真早就跑完却没人收尾的情况。所以调度员必须定时巡检,而且巡检结果要带时间打印给用户看。
**为什么是调度员而不是大脑做**:大脑的工具是只读的,看不到 agent 进程、也发不了消息;巡检是"手"的活。但"要不要改派、改派成什么"仍然是大脑的判断——调度员只做唤醒和事实汇报。
### 怎么做
1. **启动大脑之后立刻建一个每 30 分钟的巡检**(CronCreate,`recurring: true`,分钟选 `*/30` 之外的错峰值,如 `7,37 * * * *`;提示词写明"claude-brain 巡检"和运行目录)。cron 只在会话空闲时触发,这正好是 worker 可能空等的时段。到 DONE 时用 CronDelete 删掉。注意 cron 任务 7 天后自动过期。
2. **每次巡检**在工程根目录跑 `scripts/check_agents.sh <运行目录>`:它扫描本会话所有子 agent 的转录文件,列出每个 agent 的 ID、角色描述、最近一次活动时间、空闲分钟数、最后一个动作是否是"等待"(如 Monitor/后台任务),并按阈值标出 `空闲超限`。
3. **把结果带时间戳打印给用户**:一行 `巡检 HH:MM`,然后是 `agents.md` 全表,再加一张"运行中 agent 的空闲时长"小表。大脑在等报告属于正常空闲,不算超限;worker 空闲才算。
4. **发现 worker 空闲 ≥ 30 分钟且报告文件还不存在,立即介入**,按梯度:
- 第一步(唤醒):调度员先用只读手段核实它在等的事情现在怎样了(报告文件、它等待的进程/日志/输出文件是否已到终态),然后 SendMessage 给该 worker:说明"你自 HH:MM 起没有动作"、把核实到的事实告诉它、要求按原任务单继续并写报告。只陈述事实,不替它做任务。在 `log.md` 记一行 `dispatcher | 巡检唤醒 Tk`。
- 第二步(上报):下一次巡检它仍然没有任何新动作,或者它返回了但没有报告,就把它在 `agents.md` 标成 `失败/受阻`,把"worker 无响应,以下为其已有产物/最终回复"写进它的报告文件,然后 SendMessage 给大脑:报告路径 + "该 worker 两次巡检无响应"的事实,让大脑决定改派还是放弃。
- 调度员**不要**自己把活接过来做完,也不要擅自派新 worker——那是大脑的决定。
5. Worker 自己也要少睡:派 worker 的提示词里已经要求用 Monitor 等待而不是 sleep 循环,但 Monitor 最长 30 分钟就到期,worker 必须自己重挂;巡检就是兜这个底的。
### 防止空转
- 默认上限 **15 轮**。到了上限先暂停,把现状和大脑的最新判断告诉用户,问是否继续。
+19
View File
@@ -24,8 +24,27 @@
| R01 | 21:06 | brain | CONTINUE,派 T1(opus) T2(haiku) |
| R01 | 21:15 | worker | T1 完成 / T2 失败 |
| R02 | 21:16 | brain | ASK_USER |
| R02 | 21:37 | dispatcher | 巡检:T1 运行中(空闲 3 min)、T2 空闲 41 min 无报告 → 已 SendMessage 唤醒 |
| R02 | 22:07 | dispatcher | 巡检:T2 仍无动作 → 标失败/受阻,写占位报告,上报大脑 |
```
事件列除 `brain` / `worker` / `user` 外,调度员自己的动作(巡检、唤醒、状态核查)记为 `dispatcher`。
## 巡检打印格式(调度员每 30 分钟向用户打印一次)
```
巡检 21:37(下次约 22:07)
<agents.md 全表>
| agent | 角色/任务 | 最近活动 | 空闲 | 最后动作 | 判定 |
|---|---|---|---|---|---|
| a84cd… | R08-T1 仿真收尾 | 14:32 | 425 min | 等待 Monitor | 空闲超限 → 已唤醒 |
| a66f6… | R08b-T3 最终比对 | 21:53 | 1 min | 工具调用 | 正常 |
| a87ca… | 大脑 | 21:51 | 3 min | 等待报告 | 正常(大脑等待不计) |
```
数据来自 `scripts/check_agents.sh`。
## Rnn-brain.md
大脑回复的原文,不做任何修改。
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# claude-brain 巡检:列出本会话每个子 agent 的最近活动时间、空闲时长与最后动作,
# 标出空闲超过阈值的 worker,供调度员每 30 分钟打印并决定是否唤醒。
#
# 原理:Claude Code 把每个 subagent 的转录写在
# ~/.claude/projects/<项目slug>/<会话ID>/subagents/agent-<id>.jsonl
# 旁边的 agent-<id>.meta.json 带 description(派出时的 description 参数)与 model。
# 转录文件的 mtime 就是该 agent 最近一次活动;最后几 KB 能看出它停在什么动作上。
#
# 用法(在 claude-brain 运行时所在的工程根目录执行):
# check_agents.sh [运行目录] [--threshold 30] [--hours 24] [--session <会话ID>] [--ids id1,id2]
# 运行目录给出时,末尾顺带打印其中的 agents.md 名册。
# --ids 只看这些 agent(前缀即可,逗号分隔);不给则列出时间窗内所有有转录的 agent。
# 输出:先一行 "巡检 HH:MM",然后 markdown 表。判定列:
# 正常 / 空闲超限 / 正常(大脑等待不计) / 已结束?(回合结束且空闲超限,需对照名册)
threshold=30
hours=24
session=""
ids=""
rundir=""
while [ $# -gt 0 ]; do
case "$1" in
--threshold) threshold="$2"; shift 2 ;;
--hours) hours="$2"; shift 2 ;;
--session) session="$2"; shift 2 ;;
--ids) ids="$2"; shift 2 ;;
*) rundir="$1"; shift ;;
esac
done
cwd="$(pwd -W 2>/dev/null || pwd)"
slug="$(printf '%s' "$cwd" | sed -e 's#[:/\\_]#-#g')"
proj="$HOME/.claude/projects/$slug"
if [ ! -d "$proj" ]; then
echo "找不到项目转录目录: $proj" >&2
echo "请在 claude-brain 运行时所在的工程根目录执行本脚本。" >&2
exit 1
fi
mins=$((hours * 60))
if [ -n "$session" ]; then
files=$(find "$proj/$session/subagents" -name 'agent-*.jsonl' 2>/dev/null)
else
files=$(find "$proj" -path '*/subagents/agent-*.jsonl' -mmin "-$mins" 2>/dev/null)
fi
now=$(date +%s)
echo "巡检 $(date +%H:%M)(阈值 ${threshold} min,时间窗 ${hours} h)"
echo
echo "| agent | 角色/任务 | 模型 | 最近活动 | 空闲 | 最后动作 | 判定 |"
echo "|---|---|---|---|---|---|---|"
over=0
# 按 mtime 新→旧排序
for f in $(ls -t $files 2>/dev/null); do
id=$(basename "$f" .jsonl | sed 's/^agent-//')
if [ -n "$ids" ]; then
hit=0
for p in $(printf '%s' "$ids" | tr ',' ' '); do
case "$id" in "$p"*) hit=1 ;; esac
done
[ $hit -eq 1 ] || continue
fi
meta="${f%.jsonl}.meta.json"
desc="?"; model="?"
if [ -f "$meta" ]; then
desc=$(grep -o '"description":"[^"]*"' "$meta" | head -n1 | sed 's/"description":"//;s/"$//')
model=$(grep -o '"model":"[^"]*"' "$meta" | head -n1 | sed 's/"model":"//;s/"$//')
fi
mt=$(stat -c %Y "$f")
idle=$(( (now - mt) / 60 ))
last=$(date -d "@$mt" +%H:%M)
tailtxt=$(tail -c 6000 "$f")
if printf '%s' "$tailtxt" | grep -q '"name":"Monitor"'; then
act="等待 Monitor"
elif printf '%s' "$tailtxt" | grep -q '"run_in_background":true'; then
act="等待后台命令"
elif printf '%s' "$tailtxt" | grep -q 'Monitor expired\|Monitor event'; then
act="收到 Monitor 事件后停住"
elif printf '%s' "$tailtxt" | tail -c 1500 | grep -q '"stop_reason":"end_turn"'; then
act="回合结束"
elif printf '%s' "$tailtxt" | grep -q '"type":"tool_use"'; then
act="工具调用中"
else
act="活动中"
fi
isbrain=0
case "$desc" in *大脑*|*brain*|*Brain*) isbrain=1 ;; esac
if [ $isbrain -eq 1 ]; then
verdict="正常(大脑等待不计)"
elif [ "$idle" -ge "$threshold" ]; then
if [ "$act" = "回合结束" ]; then verdict="已结束?(对照名册;运行中则为空闲超限)"; else verdict="**空闲超限**"; fi
over=$((over + 1))
else
verdict="正常"
fi
printf '| %s… | %s | %s | %s | %s min | %s | %s |\n' "${id:0:5}" "$desc" "$model" "$last" "$idle" "$act" "$verdict"
done
echo
if [ "$over" -gt 0 ]; then
echo "有 $over 个 agent 空闲 ≥ ${threshold} min。对照 agents.md:状态为「运行中」且报告文件不存在的,按 SKILL.md「30 分钟巡检」第 4 步介入。"
else
echo "无空闲超限的 agent。"
fi
if [ -n "$rundir" ] && [ -f "$rundir/agents.md" ]; then
echo
cat "$rundir/agents.md"
fi