@mcd0luo/celes-dsh-watchdog
v0.1.1
Published
Celes 调度中心 DSH 插件:单例会话巡检看门狗(lock 文件 + flock 全局单例;session.list 巡检卡死/实例死亡并汇报主 worker 会话,冷却去重不轰炸)
Readme
@mcd0luo/celes-dsh-watchdog
Celes 调度中心 DSH 插件:单例会话巡检看门狗(host-only,无 client 半部)。仅面向 DSH web profile
宿主使用(经 ctx.get("apiProxy") 走内部 RPC 语义;经 HTTP 调各实例的 session.list 信封 RPC 与
session.prompt 唤醒投递 / celes-center-session-bridge 汇报通道)。
定位
4 个 DSH 实例(默认 127.0.0.1:3180-3183)都安装本插件,但全局只有一只 watchdog 在巡检—— 用 lock 文件 + flock 单例互斥;抢到锁的实例跑巡检并向主 worker 会话汇报,其余实例静默待命、 周期重试抢锁;原锁持有者进程死亡时锁自动释放,其他实例自动接管。
能力
- 巡检循环:每
intervalSeconds(默认 60s)一轮,对每个配置端口逐个调session.list(client-request 信封 RPC,HTTPPOST /api/session.list,与 celes-worker-spawn 同款信封:{type:"client-request", rpcId, method:"session.list", payload:{}})。 - 卡死检测:
running=true的非 blank 会话,复合产出信号全部冻结超过stuckThresholdMinutes(默认 15min)→ 卡死。复合产出信号 = sessionStats 的turns/steps/decodeTokens/llmMs/decodeMs,任一增长即视为仍在产出并重置计时—— 避免长 turn / 多 step 的单 turn worker 因 turns 冻结被误判(W160 审计已坐实)。长 turn (单 turn 内多 step 或已有生成产出)时阈值 ×longTurnMultiplier(默认 2)宽限。 空闲 worker 检测(W149):running=false(非 blank,已发生 turn/start)且标题匹配 worker 命名W<数字>且updatedAt距今 ≤idleThresholdMinutes(默认 10min)的形态判定; 附加产出冻结前提 + 连续 sweep 确认——已产出真实工作(steps>0 或 decodeTokens>0)的会话 需连续idleConfirmSweeps(默认 2)轮 sweep 才确认 idle,且排除 reportTo 主会话, 避免长 turn 的 turn 间隙 / 协调·汇报会话被误唤醒;仅 registry RUNNING 的在途 worker 才会被 自动唤醒(开关wakeIdleWorkers)。 - 失败检测(实例进程死亡 =「进程已死但列表残留」):对某实例的
session.listRPC 传输层失败 (连接拒绝/超时等)→ 该 DSH 进程已死,其会话残留于磁盘、列表不可服务 → 实例级失败。 W233:dsh 0.1.2+ 的/api/*挂浏览器会话 cookie 认证篱笆——匿名信封 RPC 返回 HTTP 401/403 = 实例进程活着但不可匿名观测(例:lab 3182 升级 0.1.2 后被误判 dead)。判定逻辑对 401/403 单列gated(跳过巡检、不列 dead、不升级汇报),仅连接拒绝/超时/5xx 判实例死亡。 W237 判死去抖:连接拒绝/超时/5xx 需连续deadConfirmCount(默认 3)次失败才判实例死亡—— 单次瞬失败(连接复用陈旧连接 RST、实例重载 head-of-line 卡顿、瞬时超时)只记日志不误报; 期间任何一次成功或 401/403 都清零计数。探活 RPC 走独立超时sessionListTimeoutMs(默认 30000ms,长于其他 RPC 的rpcTimeoutMs10000ms),避免大实例/重载下 session.list 慢响应被误判为死。 - 汇报(唤醒 + 巡检报告):汇总卡死/失败清单 → 一条消息注入主 worker 会话(
reportTo,默认session-a72078e7-b3c3-4ee1-a586-ca2c6aabca1f)——主通道session.promptRPC(mode:"steer", 内建唤起):目标会话非 running(离线/挂起)时先拉起 agent 再投递,报告正文标注 「这是一条唤醒 + 巡检报告」。去重:同会话未恢复时按reportCooldownMinutes(默认 60min) 冷却,不轰炸;已恢复会话状态清除,复发立即重报。 - 单例:lock 文件 + flock(默认
/tmp/celes-dsh-watchdog.lock)。 - 职责边界(检测 + 报告 + 自动唤醒):卡死/空闲会话先自动唤醒(
wakeThreshold次,默认 3), 达阈值才升级汇报主会话(reportTo);除此之外绝不 restore / kill / retry / 改动会话内容, 修复动作由主会话决策。
判定依据(对 dsh-host-apiproxy 源码核实,2026-08-31)
session.list 的 SessionSummary 行(lib/types/api/sessions.schema.js / lib/index.js)字段仅:
sessionId / updatedAt / running / blank / parentSessionId? / origin? / cwd? / agentPreset? / projections?updatedAt = max(header.createdAt, lastPromptAt)(sessionListUpdatedAt);lastPromptAt只被source.kind === "user"的user/message事件推进(applySessionListMetadata)。session.prompt注入的消息 source.kind 正是 "user"(host-apiproxy session.prompt 实现),故 worker 简报 prompt 会推进 updatedAt——running 会话的 updatedAt 停止推进 = 该轮 turn 无新 prompt,即无进展。running = agent.status === "running"(dsh-agent-loop get status:phase.kind 仅 idle|running 两态)。blank = 从未发生 turn/start。projections.values.sessionStats提供复合产出计数器:turns(已完成 turn 数)、steps(agent step 数)、decodeTokens(生成 token 累计)、llmMs/decodeMs(累计耗时)。 卡死/空闲判定以「steps/decodeTokens/llmMs/decodeMs 任一增长 = 仍在产出」为准; turns 是 turn 级粒度,长 turn 单 turn 内长期冻结,不能单独作为产出信号(W160 审计结论)。
失败信号的调研结论:SessionSummary 没有 error/status 字段(agent/error 是事件流信号, session.list 上不可观测;agent status 仅 idle|running 两态),因此会话级「错误态」在本插件的 数据边界(session.list)上不可实现;可实现的失败信号是 传输层实例死亡——RPC 连接失败 = 进程已死、会话残留不可服务("进程已死但列表残留"的实例级表达)。报告里已如实写明该判定依据。
单例方案(flock)
利用 util-linux flock(1)(零 npm 依赖、真 flock(2) 排他语义):
spawn("flock", ["-n", <lockPath>, "-c", "cat"]),stdin 管道保持打开-n非阻塞:锁被占时 flock 立即 exit(1) → 本实例待命,按lockRetrySeconds重试抢锁;- 子进程存活超过 300ms 宽限期 = 抢锁成功(flock 持锁期间 cat 阻塞读 stdin);
- 释放:
end(stdin)→ cat EOF → flock 退出 → 内核自动放锁; - 持锁进程崩溃:内核关闭管道写端 → cat EOF → 自动放锁 → 其他实例无缝接管 (无 stale-pid 竞态、无 O_EXCL 锁文件的清理问题)。
汇报通道(唤醒 + 巡检报告)
- 主通道(自动唤醒):
session.promptRPC(HTTPPOST /api/session.prompt,client-request 信封, payload{sessionId, mode:"steer", content:[{type:"text", text}]},与 server-go sessionbridge 的 promptSend 同语义)——内建唤起(非 live resume):目标会话离线/挂起时该 RPC 拉起 agent 处理该消息,在线时 followup 排队下一轮;逐实例尝试、命中目标会话的实例才成功,不依赖锁持有 实例与目标实例是否同机。汇报目标非 running 时,报告正文首行标注「这是一条唤醒 + 巡检报告」。 - 兜底:celes-center-session-bridge HTTP
POST /celes-center/bridge/send{target, content, from}——离线会话持久投递、消息不丢,但不保证唤起(仅 live agent 存在时 followup 唤起)。 - 报告发送成功才推进冷却状态;发送失败保持旧状态,下轮自动重试(不吞报)。
- 报告内容只含 sessionId/标题(≤40 字)/空闲分钟/cwd,不含 prompt、目标等敏感内容; 运行日志与审计同样不落敏感内容。
职责边界(只检测 + 报告)
看门狗绝不自动修复:不 restore、不 kill、不 retry、不改动任何卡死/失败会话——只巡检、汇总、 唤醒主会话并汇报。修复动作一律由主 worker 会话(reportTo)决策执行。
配置(schemastery z.object,settings 命名空间 celestea-dsh-watchdog,live 热更新)
| 字段 | 默认 | 说明 | |---|---|---| | intervalSeconds | 60 | 巡检间隔(秒) | | stuckThresholdMinutes | 15 | 卡死阈值(分钟):复合产出信号全冻结超此值即判卡死(长 turn × longTurnMultiplier=2);W1075/L4 起与代码一致 | | reportCooldownMinutes | 60 | 同会话未恢复的最小重复上报间隔(分钟) | | reportTo | session-a72078e7-… | 主 worker 会话 id | | host | 127.0.0.1 | 巡检实例主机 | | ports | ["3180","3181","3182","3183"] | 巡检实例端口列表 | | lockPath | /tmp/celes-dsh-watchdog.lock | 单例锁文件(flock) | | lockRetrySeconds | 10 | 待命重试抢锁间隔(秒) | | statePath | /tmp/celes-dsh-watchdog-state.json | 去重状态(跨重启持久,tmp+rename 原子写) | | logPath | /tmp/celes-dsh-watchdog.log | 运行日志 | | reportMaxSessions | 20 | 单条汇报最多列出的会话数 | | excludeSessionIds | [] | 跳过巡检的会话 id(可选) | | idleThresholdMinutes | 10 | 空闲 worker 判定窗口(分钟) | | idleConfirmSweeps | 2 | 空闲 worker 需连续 N 轮 sweep 确认(产出过的会话,防 turn 间隙误唤醒) | | longTurnMultiplier | 2 | 长 turn(单 turn 多 step/有生成)卡死阈值放大倍数 | | rpcTimeoutMs | 10000 | 单实例 RPC 超时(毫秒,session.list 除外) | | sessionListTimeoutMs | 30000 | session.list 探活专用超时(毫秒),比 rpcTimeoutMs 长 | | deadConfirmCount | 3 | 实例判死去抖:连续 N 次 session.list 失败才判 dead | | bridgeToken | "" | 桥接/审计 HTTP token(空 = 仅信任本机回环) |
自测
node --check lib/index.js lib/core.js lib/locker.js scripts/self-test.mjs
npm test # scripts/self-test.mjs:卡死检测 / 冷却去重 / 汇报文本 / flock 单例 / apply 装配(fetch 全 stub)安装注意(沿袭系列踩坑记录)
- Config 为 schemastery
z.object(standard-schema 协议,普通对象会崩); - 命名导出
{Config, apply, inject, name}(export default 静默不挂); - package.json
files白名单含cordis.patch.yml(漏了会 boot ENOENT crash-loop); - 本包自带
dsh.bundle.patch装载点;一旦基座@mcd0luo/dsh-celestea注册了celes-dsh-watchdog成员行,须移除本包dsh块,避免 duplicate loader entry id。
许可
MIT
