作者经历一次正常关闭但留下340% CPU占用的音频进程,根本原因是会话没有退出清理机制。提出用脚本+钩子强制会话对遗留状态负责的设计方案。
进程监控显示音频守护进程占用了 340% 的 CPU。那就是全部的症状——发生在当天早些时候一个会话关闭之后的数小时,机器其他方面都正常。表象之下:326 个音频捕获流同时运行,temp 目录下有 37 GB 的原始音频,以及我的听写设置中一个维护循环——从当天早些时候的一个会话开始,每分钟泄漏一个 recorder 进程。那个会话正常结束了,什么问题都没有报告,却在身后留下了损害。我所构建的任何东西中,都没有一步要求会话为它离开后留下的局面负责。
会话随时都在不告而别。上下文窗口结束,面板被杀死,笔记本休眠,无论会话生成了什么,要么继续运行,要么停止,而两种情况都可能出错。其他会话必须尊重的锁、夜间运行剪掉的临时 worktree、recorder 进程、未提交的文件:所有这些都是比会话更长寿的状态,而后续的会话无从知晓。这和那份无人阅读的报告是同一种失败,只是穿了不同的外衣。
这就是会话结尾的机制。
设计由一个脚本和两个钩子构成,我要争取的属性落在第二个钩子上:一个被跳过的 gate 不能持续处于跳过状态。
会话结束时运行一套检查,并为项目写入一个标记文件:绿色带时间戳,或者债务带发现物清单。会话开始时读取标记。绿色且不超过一周意味着沉默:不打印任何东西,会话开始。标记缺失、过时或红色,意味着前一个会话跳过了 gate 或未能通过,新会话将发现物清单作为注入的上下文继承(start hook 的输出就是上下文,这是有文档记载的契约),并且必须在做任何其他事之前清除它们并运行 end 检查。一个在飞行中途死掉的会话不会留下标记,所以下一个会话代替它运行这套检查。没有哪条路径能让债务不被看见,这是唯一重要的属性。
session end ──▶ run the suite ──▶ marker: ok, or debt + findings
│
session start ──▶ read the marker ◀──┘
├─ ok, under a week old ──▶ silence, the session begins
└─ missing, stale or debt ──▶ findings injected as context;
clear them, run "end", then work
这套检查有意做得很小,而且每个检查都对应一个真实的事故。捕获流超过两个:326 流那类。temp 目录超过 512 MB 或二十个文件:37 GB 那类。锁超过四十八小时:持有它死掉的会话。会话结束时存在未提交路径:wrap-up 规则,以前是习惯,现在可检查了。超过五天的 sweep worktree:崩溃的夜间运行。以下是一个可运行的版本,包含前三个检查:
#!/bin/sh
# session-gate.sh end|start
MARK="$HOME/.agent-gate/$(basename "$PWD").json" # (1)
f=""
note() { f="$f - $1\\n"; }
suite() { # (2)
n=$(pgrep -f avfoundation | wc -l | tr -d ' ')
[ "$n" -gt 2 ] && note "AUDIO: $n capture streams (expect 1)"
kb=$(du -sk /tmp/capture-live 2>/dev/null | cut -f1)
[ "${kb:-0}" -gt 512000 ] && note "TMP: capture-live ${kb}KB"
d=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
[ "$d" -gt 0 ] && note "REPO: $d uncommitted paths"
}
mark() { # (3)
mkdir -p "${MARK%/*}"
state=ok; [ -n "$f" ] && state=debt
printf '{"state":"%s","when":"%s","findings":"%s"}\n' \
"$state" "$(date -u +%FT%TZ)" "$f" >"$MARK"
}
case "$1" in
end) suite; mark ;;
start) [ -f "$MARK" ] || { suite; mark; } # (4)
fresh=$(find "$MARK" -mtime -7) # (5)
grep -q '"state":"ok"' "$MARK" && [ -n "$fresh" ] && exit 0
echo "SESSION-GATE DEBT: the previous session left work."
echo "Clear it, then run: session-gate.sh end"
sed -n 's/.*"findings":"\(.*\)".*/\1/p' "$MARK" |
sed 's/\\n/\n/g' ;; # (6)
esac
标记按项目区分,所以债务只会迎接回到产生它的地方的那个会话,而不是某个无关仓库里的陌生人。
每个检查一行,每个都以来自真实事故的名字命名;套件通过复盘来增长,绝不靠想象。
每次 end 时写入,绿色或红色,所以没有标记只能意味着一个从未到达其 end 的会话。
没有标记的 start 自行运行套件,代替那个已死的会话。
绿色一周后过期;旧的绿色不是证据。
发现物原样打印,而 start hook 的输出会被注入为上下文,所以债务是新会话读到的第一样东西。
将 end 接入你的 agent 的 session-end hook,将 start 接入 session-start hook,此后每次会话都会运行它。如果那天有这个机制,下一个会话打开时会看到:
$ session-gate.sh start
SESSION-GATE DEBT: the previous session left work.
Clear it, then run: session-gate.sh end
- AUDIO: 326 capture streams (expect 1)
- TMP: capture-live 38797312KB
关于这个形态的一件事。检查是失败软且快速的;gate 不是用来阻止会话结束的,它是用来让下一个会话的第一屏说出真相的。
我可以推广的是:任何会话可能留下的资源,都需要在某处有一个计数器,而会话边界是读取它最廉价的地方。那天说出真相的计数是捕获流,326 个,而这个计数现在在每次会话结束时都会运行,无论会话是否记得。