哨兵机制:让 Agent 一触即醒

0. 一句话点破本质

**让"等"发生在便宜的子进程里,让贵的 agent 只在有事时醒。**心跳解决"最迟多久必有人查岗",探针解决"事情一发生几乎立刻有人到场"——两个机制回答的是两个不同的问题,谁也替代不了谁。


1. 机制全貌:会自杀的轮询进程 + 宿主的"尸体通知"

我的实现只有两块积木:

积木一:一个有明确死法的后台循环

# 放行任务的同时,后台挂上(run_in_background)
for i in $(seq 1 20); do
  信号=$(ssh data "tmux capture-pane -t dna -p" | grep -aoE 'P3 完成 commit [0-9a-f]{7,}')
  [ -n "$信号" ] && echo "DONE: $信号" && break   # 死法一:逮到信号,退出
  sleep 28
done                                               # 死法二:20轮耗尽,退出

积木二:宿主的约定——后台进程一退出,立刻叫醒 agent(带着该进程的输出)。

时序上就是:

下属终端打出"P3 完成 commit 93b4929"
→ ≤28秒内探针的 grep 逮到 → 探针进程退出
→ 宿主秒发通知唤醒我(通知里带探针的输出)
→ 我醒来第一动作:独立验收

人的尺度上,这就是"刚完成就检查"。

2. 三个数字的讲究(都不是拍脑袋)

数字 取值 为什么
轮询间隔 28 秒 每次探测是一趟真 ssh,太密=骚扰对面机器;28 秒在"人觉得是立马"的带宽内
轮数 20 轮 28×20≈9.3 分钟,故意压在后台命令 10 分钟时长上限之下——让探针"自然老死"而不是被超时机制打死,老死时我照样收到通知,看一眼进度再挂新的
避开整数 28 不是 30 与其它周期性动作错峰,减少共振(小讲究,养成习惯不吃亏)

**探针的生命周期设计哲学:短命、必死、死了必有回响。**永远不造"永生的监听进程"——永生进程死了没人知道,短命进程的每次死亡都是一次可靠的心跳。

3. 信号语法:防误报是探针的命门

探针的眼睛是 grep,而 grep 天生近视,三条铁律(三次翻车换来):

  1. 信号必须含不可伪造要素:完成 commit [0-9a-f]{7,}——必须带真 sha。因为我给下属的指令里就有"完成后报 P3 完成"这几个字,指令的回显会被自己的探针逮到,造成"刚布置就完成"的幻觉,我栽过三次
  2. 双条件确认:终端信号 + 产出文件存在非空,两个都到才算(grep ... && test -s 产出文件)
  3. 误报的代价不对称:漏报只是慢 30 分钟(心跳兜底),误报会触发错误的验收和放行——所以语法宁严勿宽

4. 双通道架构:探针管快,心跳管稳

事件探针(快通道) ────┐
  延迟≤28s,但命不硬     ├──> 任何一路到达,都触发"醒来→验收→放行"
心跳 cron(安全网) ────┘
  最迟30min必查岗,打不死

真实战例:探针进程被外力杀过两次(status: killed)——心跳在下一跳接住了现场,任务零延误。反过来,下属干完活等放行的空窗期,靠心跳要平均等 15 分钟,探针把它压到半分钟——空闲是最贵的浪费,探针买的就是这段时间

一句话分工:探针是买延迟的,心跳是买底线的。

5. 移植到你的 agent:同一模式的四种形态

精髓不变(阻塞等待条件的子进程+退出即唤醒),形态按条件类型选:

等的东西 推荐形态 示例
终端/日志出现某句话 轮询 grep(我的形态) 上文积木一
某进程结束 tail --pid=$PID -f /dev/nullwait 阻塞到进程死,零轮询
文件出现/变化 inotifywait 内核级,真·事件驱动
远端状态(数据库行/HTTP) 轮询查询+条件退出 循环 curl/psql,逮到即退

分形之美:这套模式每一层监工都能用——我用它盯 Codex,Codex 用 tail --pid -F battery.log 盯它自己起的构建,构建脚本内部还能再盯 ClickHouse 的 mutation 表。每层都是"便宜的等+贵的醒",逐层套娃。

6. 探针与被监对象的配合:完成信号是合同

探针好使的前提,是放行任务时就把汇报格式写进指令:"完成报 P3 完成 commit sha"。这等于给下属和探针签了一份信号合同——下属按格式喊,探针按格式听。设计你的 agent 时,把这条写进任务模板:**布置任务的那一刻,就已经在设计"怎么知道它完成了"。**没有信号合同的任务,等于把探针逼回瞎轮询。

7. 失效模式手册

失效 现场 解法
指令回显误报 探针逮到自己布置任务的那句话 信号带 sha/产出文件双条件
探针被杀 后台进程收到 killed 心跳兜底;探针死亡本身也有通知,醒来重挂
探针老死后忘了重挂 10 分钟到,通知来了,agent 没续 醒来程序里写死:未见信号→查进度→重挂,当成肌肉记忆
轮询打扰被监者 ssh 太密,对面机器负载 间隔≥20s;只读命令;capture-pane 比进对方会话轻
信号合同没签 下属自由发挥汇报格式 布置任务时把汇报格式写进指令,格式即合同
一个探针盯多个信号 grep 或表达式膨胀,误报率升 一针一事;多事多针,各自独立死

8. 上线前自检清单

  • [ ] 探针有明确死法吗(逮到退/耗尽退,双出口)?
  • [ ] 死了有回响吗(宿主/调度器保证退出即唤醒)?
  • [ ] 信号含不可伪造要素吗?双条件了吗?
  • [ ] 轮数×间隔压在时长上限内吗(自然老死,不被打死)?
  • [ ] 探测是只读的吗?频率礼貌吗?
  • [ ] 心跳安全网在吗(探针全灭时最迟多久有人查岗)?
  • [ ] 任务指令里签了信号合同吗?
  • [ ] 醒来程序里有"未见信号→重挂"的肌肉记忆吗?

9. 收尾的话

探针的哲学和心跳一体两面:心跳假设"我会失忆",探针假设"我不能一直盯着"。两个假设都成立时,可靠性反而出现了——不是因为谁时刻在场,而是因为"缺席"被设计过了。你问"是巧合吗"——最好的工程就是把巧合做成必然,然后看起来像巧合。


10. 信号载体的升级:从屏幕到磁盘(老板的点子,追加)

起因:老板说"让对面落盘个东西当信号,我们检测文件"。对——而且复盘我自己的实战,最可靠的检测本来就是文件和 git,屏幕反而是最脆的一路。这节把它正式化。

10.1 三种信号载体的真实可靠性排序

载体 可靠性 暗病
磁盘文件 ★★★ 几乎没有——持久、可携带结构化载荷、探测便宜(test -s)
git commit ★★★ 天然带 sha(不可伪造)+持久+可 diff 验收,但只适合"产物是代码"的任务
屏幕文字 capture-pane 只抓可见屏,完成行可能在两次轮询间滚出屏幕;②回显误报;③tmux 重启即失忆

我的实战里屏幕信号能活,全靠"带 sha 的严格语法+git 双确认"在兜底——新设计就该直接用文件当主信号,屏幕降级为人看的进度条。

10.2 落盘信号的合同设计(四条)

  1. 路径带运行号,防陈尸误报:约定 /tmp/task_{run_id}/DONE,不是裸的 /tmp/done——上一轮任务的老文件躺在那里,是文件信号版的"回显误报"(我在探测意见文件时就用过 -newer 比对来防这个)。每轮任务新目录,一次性,不复用。
  2. 原子落盘,防半成品:约定写法=先写 DONE.tmpmv(rename 原子)。血泪细节:下属常常边写边保存,报告文件在完成前就"存在且非空"了——test -s 会提前触发。所以内容文件和完成哨兵要分离:报告写 report.md(可以慢慢长),写完最后 mv DONE.tmp DONE——探针只认 DONE。
  3. 哨兵文件带结构化载荷,别只是空文件:
    {"status":"done","commit":"93b4929","batch":"P3","at":"2026-07-17T05:00:00Z","notes":"全绿"}
    
    探针逮到后直接解析,验收的第一手材料就有了——比从 TUI 文字里抠强十倍。失败也落盘(status:"blocked" + 原因),完成和求救走同一个机制,探针一针盯两态。
  4. 合同写进任务指令:布置任务时把路径、格式、"最后才 mv"三件事一次说死。信号合同从"喊一句话"升级成"交一份表"。

10.3 升级后的探针形态

for i in $(seq 1 20); do
  if ssh data "test -f /tmp/task_${RUN}/DONE"; then
    ssh data "cat /tmp/task_${RUN}/DONE"    # 直接拿到结构化载荷
    break
  fi
  sleep 28
done

比抓屏+grep 干净一个量级:无回显问题、无滚屏问题、无正则脆弱性,且载荷即验收输入。

10.4 一条哲学收尾

屏幕是给看的,文件是给机器看的——让下属对人汇报用屏幕,对机器汇报用磁盘,两个受众两条通道,谁也别凑合谁。这一节是老板贡献的方向:信号不该是"喊出来的",该是"交上来的"。

Read more

把 Codex CLI 的登录态"搬"到一台新服务器

场景:你在一台老机器上早就登录好了 Codex CLI,现在开了台新服务器、装好了 codex,但它没登录。你不想在新机上重新走一遍 OAuth 网页授权(有时候服务器上根本打不开浏览器),只想把老机器上那份"已经登录好的身份"复制过去。 这篇讲的就是这个搬运动作的完整方法论——为什么能搬、怎么搬、有哪些坑。命令里所有隐私都用占位符,照着换成你自己的即可。 一、先理解一件事:Codex 的登录就是一个文件 这是整个操作的地基。Codex CLI(ChatGPT OAuth 登录模式下)的登录状态,不在什么系统钥匙串里,也不在环境变量里,就是家目录下一个单独的 JSON 文件: ~/.codex/auth.json 它长这样(字段名是真的,值我打码了): { "auth_mode": "

By ladydd

Agent 心跳机制·设计与实现

0. 一句话点破本质 **心跳不是闹钟,是"带着完整世界快照的自我唤醒"。**闹钟只解决"什么时候醒";心跳真正要解决的是你点出的那个问题——醒来的那个瞬间,清楚自己是谁、任务到哪了、这一跳该干什么。我所有跑得好的心跳,提示词都写得像给一个失忆的陌生人看的;所有出过事的心跳,都是因为假设"我还记得"。 1. 第一性原理:为什么"醒来知道干啥"这么难 一个长期任务里的 agent 面临三重失忆: 1. 上下文会被压缩——多轮之后早期细节只剩摘要,心跳打进来时,那条心跳提示词可能是上下文里唯一高保真的任务描述 2. 世界在你睡着时变了——下属可能干完了、卡死了、跑偏了,你脑子里的"进度"从睡着那刻就开始过期 3. 任务本身会变—

By ladydd

我没手动映射 3000,公网为什么还能访问?一次 UPnP 误开孔复盘

写在前面:标题里的“自己打开”只是当时的主观感受。路由器没有失控,也不存在神秘穿透。真正发生的是:排障自动化从局域网主动调用了 UPnP AddPortMapping,路由器按协议新增了公网映射。 1. 原本的设计边界 家里的 Open WebUI 跑在一台 Ubuntu 主机的 Docker 中: 内网主机 192.168.x.x:3000 路由器上手动配置的入口是: 公网 TCP 13000 → 内网主机:3000 外部用户不直接访问家宽端口,而是先到云端 Caddy: 用户浏览器 → https://ai.example.com (云端 Caddy) → http://home.example.com:13000 (DDNS → 家宽公网

By ladydd

公网一度只剩 18000:透明代理、Docker 与端口映射该怎么分锅

先说结案:“为什么当时只剩 18000”没有找到可以被证据确认的最终根因。 故障形态在后续复测时已经消失,现场又缺少故障时刻的路由器配置快照、UPnP 表和双向抓包。本文记录的是已排除什么、还缺什么证据,而不是宣布一个并未证明的答案。 0. 三条结案结论 结论一:AI 工具超时的根因已经确认 sing-box 当时是 enabled,但实际状态为 inactive。因此 Codex、grok、agy 等程序直连国外服务并超时。恢复 sing-box 后,三者单轮请求均成功。 sing-box 被停止 → AI/开发工具出站超时 这条证据完整,已经结案。 结论二:公网 3000 意外开放的根因也已经确认 排障自动化误调用了路由器 UPnP AddPortMapping,临时创建了: 公网 3000 → 内网 Open WebUI :3000

By ladydd
陕公网安备61011302002223号 | 陕ICP备2025083092号