112 lines
4.2 KiB
Bash
112 lines
4.2 KiB
Bash
#!/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
|