LifeOS CMUX Monitor 工作流:基于轮询的 Agent 状态监控与语音通知实战指南
【免费下载链接】LifeOS⛰️ The Life Operating System — an intent engineering platform that moves you from your current state to your ideal state, in life and work.项目地址: https://gitcode.com/GitHub_Trending/pe/LifeOS
导读
monitor是 LifeOS 的 CMUX 技能中负责"观察—改进"闭环的工作流:它定期轮询一个工作区(workspace)内的所有 surface,把每个 Agent 的状态分类为 idle / working / done / awaiting-input,并在某个 Agent 刚刚完成或需要你输入时,通过 Pulse 的/notify接口触发 {{DA_NAME}} 语音播报。读完本文,你将掌握 monitor 的完整调用方式、四态分类的底层判定逻辑、状态迁移触发语音的实现原理,以及如何把它接入 Agent 竞速、多团队编排等实战场景,做到"只看该看的,不盯不需要盯的"。
为什么需要 monitor:看不见的 Agent 无法被改进
在 cmux 组成的多 Agent 工作区里,一个你看不见的 Agent 就是一个你无法改进的 Agent。当一个 8 个 Agent 的团队分散在多个你从不查看的窗格中时,它们实际上只是 8 个静默的黑盒——只有当你碰巧瞥一眼时,才会发现它们卡住了、死循环了,或者早就完成了。
monitor正是为填补这一空白而设计:它替你在每个 surface 上持续盯守,并在"值得你注意"的状态迁移发生时主动发声,从而把你的注意力导向真正需要它的 Agent,而不是花在照看那些不需要照看的 Agent 上。这一点与 CMUX 技能的定位一脉相承——SKILL.md 开篇即强调:"an agent you can't see is an agent you can't improve",而monitor就是让"看得见"这件事自动化的工具。
设计前提:poll-not-event 的现实
cmux 没有任何 push 或事件订阅命令,也没有"完成时通知我"的回调可以注册。因此monitor采用轮询模型:
- 每隔
--interval秒,遍历工作区中的每个 surface; - 对每个 surface 调用
surface-health,并读取屏幕尾部(read-screen); - 将本次状态与上一轮的结果做 diff。
所谓"通知",是轮询循环检测到的状态迁移,而不是应用主动发出的事件。这是 cmux CLI 本身的设计约束,而不是封装层的缺陷。这一设计取舍在 DESIGN.md 中被明确记录:"cmux has no push or event-subscribe command... 'Agent finished' is discovered by pollingsurface-health+read-screenand matching idle/done markers. We design the monitor as a poll loop, full stop."(cmux 没有订阅命令,"Agent 完成"只能通过轮询 surface-health + read-screen 并匹配 idle/done 标记来发现,monitor 就是一个彻头彻尾的轮询循环。)
核心用法:启动监控循环
在封装层 Tools/cmux.ts 中,monitor子命令的入口为commandMonitor(cmux.ts),基础用法如下:
bun ~/.claude/skills/CMUX/Tools/cmux.ts monitor --workspace beta --interval 3参数说明:
| 参数 | 含义 | 默认值 |
|---|---|---|
--workspace <ref> | 要监控的工作区引用(如workspace:7、beta) | 全部 surface |
--interval <N> | 两轮轮询之间的间隔秒数,必须是正整数 | 3秒 |
--once | 只做一轮分类便退出(适合脚本化定点巡检) | 循环运行直至 Ctrl-C |
间隔参数由parsePositiveInteger校验(cmux.ts),非法值会返回错误--interval must be a positive integer。循环期间按 Ctrl-C(SIGINT)会输出{"ok":true,"stopped":true}后干净退出。
每一轮中,monitor 对每个 surface 依次执行surface-health与read-screen(读取尾部40行,常量MONITOR_TAIL_LINES = 40,见 cmux.ts),然后将结果交给分类器,得到以下四类状态:
| 状态 | 含义 | 典型标志 |
|---|---|---|
idle | shell 提示符,没有活跃任务 | 行尾为$、%、#、❯等提示符 |
working | 输出仍在滚动 / 进程仍在运行 | 不匹配其他任何类别的兜底状态 |
done | 尾部出现完成标记 | 绿色测试通过、"done"、"completed"、exit code: 0、✓/✔ 符号 |
awaiting-input | 有一个提示正等着你回应 | y/n、(y/n)、press enter、confirm、continue?或行尾? |
四态分类的源码级实现
分类逻辑集中在classifyScreen函数(cmux.ts),判定顺序本身就是优先级设计:先判awaiting-input,再判done,再判idle,最后兜底为working。注意done的优先级低于awaiting-input——如果屏幕尾部既有完成标记又有提问,优先视为需要你介入。
function classifyScreen(text: string): MonitorStateName { const tail = text.trimEnd(); if (/(do you want|\[y\/n\]|\(y\/n\)|press enter|confirm|continue\?)/i.test(tail) || /\?\s*$/.test(tail)) { return "awaiting-input"; } if (/(^|\n)(done|completed|exit code:\s*0)\b/i.test(tail) || /[✓✔]\s*(done|complete|completed)?/i.test(tail)) { return "done"; } if (/(^|\n)[^\n]*([$%#❯])\s*$/.test(tail)) { return "idle"; } return "working"; }分类的输入来自readMonitorStates(cmux.ts):
- 先调用
surface-health拿到工作区所有 surface 的引用列表; - 若拿不到任何 surface 引用,则退化为对整体聚合输出分类(以 workspace 引用代替 surface 引用);
- 否则对每个 surface 单独执行
read-screen --lines 40; - 读取失败的 surface 一律标记为
working(保守处理,避免误报完成); - 输出结构为
{ ok: true, states: [{ ref, state, textTail }] },循环模式下每轮输出一行 JSON。
这里可以做一个值得注意的推断:分类完全依赖屏幕尾部的文本启发式,因此它在不同 shell、不同 Agent CLI 之间的表现会有差异——这正是 DESIGN.md 风险清单中明确指出的"done 检测是启发式的,且跨 shell 容易脆弱"。在真正的生产依赖落地前,建议用send → read回读验证来双确认。
状态迁移触发与语音通知
monitor 的核心价值不在于"知道状态",而在于只在状态发生迁移时通知。循环内部维护了一个previous: Map<ref, MonitorStateName>(cmux.ts),每一轮对比新旧状态,仅当满足以下条件时才发声:
const oldState = previous.get(state.ref); if (oldState !== state.state && (state.state === "done" || state.state === "awaiting-input")) { const label = state.state === "done" ? "done" : "awaiting input"; await notifyVoice(`cmux surface ${state.ref} is ${label}`); } previous.set(state.ref, state.state);即:只有"从其他状态迁移到 done 或 awaiting-input"才触发通知。这带来两个重要的工程收益:
- 状态必须跨轮保持,一次画面闪烁不会立刻产生通知——相当于内置了一重去抖(debounce)机制,避免"一帧抖动就刷屏";
- 已经处于 done / awaiting-input 的 surface 不会重复播报,只有新的迁移才会发声。
通知动作由notifyVoice(msg)(cmux.ts)完成,它是一个 fire-and-forget 的 HTTP POST,目标地址由常量VOICE_URL = \${PULSE_BASE}/notify`` 决定:
POST http://127.0.0.1:31337/notify { message, voice_enabled: true }请求体携带{ message, voice_enabled: true },并设置了 5 秒的超时(AbortSignal.timeout(5_000)),失败时静默返回false,不会拖垮轮询主循环。
文档中描述的语音内容是beta/worker-2 finished或beta/lead awaiting input这类带角色语义的消息;从源码看,当前封装层实际发送的模板为cmux surface <ref> is done与cmux surface <ref> is awaiting input,即"surface 引用 + 状态"的机器可读格式——如果你希望播报更人性化(比如带上 Agent 的角色名),可以在外层把 workspace/surface 引用映射为团队角色后再调用。
Pulse 端如何接收
PULSE_BASE定义在 endpoint.ts:
export const PULSE_BASE = process.env.PULSE_URL ?? "http://127.0.0.1:31337"- 默认硬编码为 IPv4 字面量
127.0.0.1:31337,这是有意为之:Pulse 只绑定 IPv4,macOS 上localhost会同时解析到::1与127.0.0.1,Happy-Eyeballs 可能先尝试失效的 IPv6 地址造成卡顿,硬编码 IPv4 可消除该停滞; - 可用环境变量
PULSE_URL覆盖(如 Pulse 运行在其他主机/端口、或远程 fleet 成员); endpoint.ts刻意零依赖,方便 hooks 与技能工具直接 import。
语音端点在 VoiceServer/voice.ts 中处理POST /notify:message缺省为"Task completed",voice_enabled !== false决定是否发声,title缺省为"LifeOS Notification";POST 路由还带有基于客户端 IP 的限流(checkRateLimit)与 CORS 预检处理。TTS 的播放通过串行队列(playbackQueue)排队,保证并发 /notify 不会在扬声器上重叠(voice.ts)。
单轮巡检:--once 模式
如果不想长期驻留循环,而只是做一次"定点体检"(例如放在另一个工作流内部、或作为boot-team后的即时确认),使用--once:
bun ~/.claude/skills/CMUX/Tools/cmux.ts monitor --workspace beta --once该模式完成一轮surface-health+read-screen+ 分类后立即输出结果并退出(return 0),不进入sleep等待。它同样执行迁移判定——第一轮previous为空,因此不会触发任何语音通知,只会输出{ ok: true, states: [...] }的 JSON。典型用法见 BootTeam.md 的实战示例:向 lead 派发任务后,用monitor --workspace workspace:5 --once快速确认整个团队的状态,再把长期盯守交给循环模式的monitor。
monitor 如何喂给 Pulse 与语音
需要强调的是:monitor不是要替代 LifeOS 的 Pulse 仪表盘,而是喂给它。分类出的 surface 状态流向 Pulse(默认127.0.0.1:31337),与旧的 Kitty 标签状态层上报 working/done/awaiting 的方式同源;完成消息则走既有的/notify → {{DA_NAME}} TTS语音通道。也就是说:
- cmux 是"新被盯守的 surface 层";
- Pulse 与语音保持原样不动;
- 整体链路是"状态进,仪表盘 + 语音出"。
这与 DESIGN.md 的核心设计判断一致:几乎整个 LifeOS 都构建在终端之上——Pulse 读 work.json、语音打 HTTP 端点、算法写 phase 到注册表,都不关心终端是 Kitty 还是 cmux;唯一真正耦合 Kitty 的是标签状态绘制层,而 cmux 只替换终端盯守这一层。monitor 对应的正是设计文档中 Phase 2 的"cmux 状态 → Pulse 桥接"(DESIGN.md 的 Phased rollout 一节),其风险被标注为中等:轮询成本与标记启发式误报——分类器太吵会刷爆语音,这正解释了上面"只在迁移时发声"的设计。
实战示例:无值守看护一场 Agent 竞速
monitor最典型的落地场景是 Agent 竞速(race):多个 Agent 同时进攻同一个问题,谁先解决谁赢(详见 AgentRace.md)。当你同时跑 5 个 Agent 时,不可能一直盯着屏幕等第一个完成者——交给 monitor 即可:
# 一个 5 Agent 的竞速正在 workspace:7 中运行(参见 AgentRace.md) bun ~/.claude/skills/CMUX/Tools/cmux.ts monitor --workspace workspace:7 --interval 2 # ... 你可以去忙别的事 ... # {{DA_NAME}}: "cmux surface surface:32 is done" <- 第一个完成,语音立即响起听到播报后,拉取胜者并标记:
# 读取胜者 surface 的尾部 80 行(--lines 缺省即为 80) bun ~/.claude/skills/CMUX/Tools/cmux.ts read --surface surface:32 --lines 80 # 对整个工作区触发视觉闪烁,把注意力引导过去 bun ~/.claude/skills/CMUX/Tools/cmux.ts flash --workspace workspace:7read子命令对应源码中的commandRead(cmux.ts),底层调用read-screen --lines N,--lines缺省 80;flash底层调用trigger-flash(cmux.ts)。竞速场景的完整闭环(启动 → 轮询 → 读取胜者 → 关闭败者 surface)在 AgentRace.md 中有完整的五步说明,其中第 3 步正是 monitor 轮询。
与团队编排工作流的协同
monitor 盯守的"团队"来自两条路径:
- 分层团队(tiered):
boot-team --tiers orchestrator,lead,worker,worker在一个工作区铺开三层团队,见 BootTeam.md; - 网格舰队(grids):
fleet --name beta --grid 2x2搭建本地网格,mini-fleet打开远程 SSH 窗格,见 Fleet.md。
在 Fleet.md 的实战示例中,一个完整的全栈特性团队(本地 2x2 网格 + 远程 mini-fleet)最终统一用一条命令盯守:
bun ~/.claude/skills/CMUX/Tools/cmux.ts monitor --workspace beta --interval 3而 SKILL.md 的 Quick Reference 与示例也把monitor --workspace <ws>作为"lead 回报时语音通知"的标准收尾动作。
前置条件与已知局限
在真正把 monitor 投入生产前,需要了解以下来自 SKILL.md 与 DESIGN.md 的事实与限制:
- Mac-only:cmux 是 macOS 应用,没有 Linux/WSL 版本,对应路径是 tmux;远程 fleet 通过本地 SSH 窗格驱动,不需要远端安装 cmux。
- Socket 默认拒绝(#1 Gotcha):cmux 的 socket 默认 deny,外部进程会被拒绝连接。两种通过方式:(a) 在 cmux surface 内部运行编排器(继承认证);(b) 在 cmux Settings 设置 socket 密码并导出为
CMUX_SOCKET_PASSWORD(封装层通过buildCmuxCommand自动附加--password,见 cmux.ts)。安全提示:持有 socket 密码的本地进程可以驱动你的整个 Agent 舰队,务必谨慎设置且不要提交到公开文件。 - 自动拉起:若 socket 不存在,封装层会自动
open -a cmux拉起应用,并以 750ms 间隔轮询ping最长 15 秒(PING_WAIT_MS = 15_000),超时则明确报错而非静默失败(cmux.ts)。 - 状态验证状态:据 SKILL.md 的 2026-07-07 状态说明,封装层本身已通过类型检查(
tsc/bun build)、voice已实测可用,但 monitor 的 live 驱动(socket-auth 握手)尚未执行线上验证,属于"离线验证通过、线上待证"状态——部署前请先在 cmux surface 内部或设置好CMUX_SOCKET_PASSWORD的情况下跑通一次。 - 轮询成本与调优:DESIGN.md 指出间隔是一个调优旋钮:太密烧 CPU 且容易刷屏语音,太疏则"完成"通知滞后(默认约 3 秒)。分类器需要去抖,而"仅在迁移时发声"的 previous-map 判定正是该去抖的实现。
- 引用是位置性的:
workspace:1/surface:2这类索引会随开合而漂移;对于长期存活的盯守对象,建议先用tree配合--id-format uuids解析出 UUID 并持有(SKILL.md Gotchas)。 - 优先 hooks,轮询是兜底:SKILL.md 特别说明,对 Claude Code 启动的 Agent,cmux 会自动注入生命周期 hooks(
SessionStart/Stop/Notification/UserPromptSubmit → cmux claude-hook <event>),Agent 会主动上报状态;monitor 的surface-health+read-screen轮询是非 Claude Agent 的兜底路径,而非首选通道。
小结
monitor把"盯着屏幕看 Agent"这件重复劳动,抽象成一条可审计、可脚本化的观察循环:轮询surface-health与read-screen→ 四态启发式分类 → previous-map 迁移判定 →/notify语音播报。它不替代 Pulse 仪表盘,而是作为新的 surface 状态源把 cmux 接入既有的"状态进、仪表盘 + 语音出"链路。无论是无值守看护 Agent 竞速、巡检三层团队,还是盯守远程 mini-fleet,一条monitor命令即可把注意力还给真正需要它的 Agent——这正是"observe-to-improve"闭环的落地点。
【免费下载链接】LifeOS⛰️ The Life Operating System — an intent engineering platform that moves you from your current state to your ideal state, in life and work.项目地址: https://gitcode.com/GitHub_Trending/pe/LifeOS
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考